UI overhaul

This commit is contained in:
seniorswe
2025-09-22 20:53:33 -04:00
committed by seniorswe
parent 8c657a4f49
commit 3edac408a0
27 changed files with 6284 additions and 5659 deletions
+20
View File
@@ -8,7 +8,9 @@
"name": "doorman",
"version": "0.1.0",
"dependencies": {
"clsx": "^2.1.0",
"date-fns": "^4.1.0",
"lucide-react": "^0.460.0",
"next": ">=15.3.3",
"react": "^19.0.0",
"react-dom": "^19.0.0"
@@ -2061,6 +2063,15 @@
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
"license": "MIT"
},
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/color": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
@@ -4162,6 +4173,15 @@
"dev": true,
"license": "ISC"
},
"node_modules/lucide-react": {
"version": "0.460.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.460.0.tgz",
"integrity": "sha512-BVtq/DykVeIvRTJvRAgCsOwaGL8Un3Bxh8MbDxMhEWlZay3T4IpEKDEpwt5KZ0KJMHzgm6jrltxlT5eXOWXDHg==",
"license": "ISC",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+2
View File
@@ -9,7 +9,9 @@
"lint": "next lint"
},
"dependencies": {
"clsx": "^2.1.0",
"date-fns": "^4.1.0",
"lucide-react": "^0.460.0",
"next": ">=15.3.3",
"react": "^19.0.0",
"react-dom": "^19.0.0"
File diff suppressed because it is too large Load Diff
+210 -426
View File
@@ -1,456 +1,240 @@
'use client'
import React, { useState } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import '../apis.css';
import './add-api.css';
interface CreateApiData {
api_name: string;
api_version: string;
api_description: string;
api_allowed_roles: string[];
api_allowed_groups: string[];
api_servers: string[];
api_type: string;
api_allowed_retry_count: number;
api_authorization_field_swap?: string;
api_allowed_headers?: string[];
api_tokens_enabled: boolean;
api_token_group?: string;
}
const menuItems = [
{ label: 'Dashboard', href: '/dashboard' },
{ label: 'APIs', href: '/apis' },
{ label: 'Routings', href: '/routings' },
{ label: 'Users', href: '/users' },
{ label: 'Groups', href: '/groups' },
{ label: 'Roles', href: '/roles' },
{ label: 'Monitor', href: '/monitor' },
{ label: 'Logs', href: '/logs' },
{ label: 'Security', href: '/security' },
{ label: 'Settings', href: '/settings' },
];
const handleLogout = () => {
localStorage.clear();
sessionStorage.clear();
setTimeout(() => {
window.location.replace('/');
}, 50);
};
import React, { useState } from 'react'
import { useRouter } from 'next/navigation'
import Link from 'next/link'
import Layout from '@/components/Layout'
const AddApiPage = () => {
const router = useRouter();
const [formData, setFormData] = useState<CreateApiData>({
const router = useRouter()
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [formData, setFormData] = useState({
api_name: '',
api_version: '',
api_description: '',
api_allowed_roles: [],
api_allowed_groups: [],
api_servers: [],
api_type: 'REST',
api_allowed_retry_count: 0,
api_tokens_enabled: false
});
const [newRole, setNewRole] = useState('');
const [newGroup, setNewGroup] = useState('');
const [newServer, setNewServer] = useState('');
const [newHeader, setNewHeader] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleInputChange = (field: keyof CreateApiData, value: any) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
const addRole = () => {
if (newRole.trim()) {
setFormData(prev => ({ ...prev, api_allowed_roles: [...prev.api_allowed_roles, newRole.trim()] }));
setNewRole('');
}
};
const removeRole = (index: number) => {
setFormData(prev => ({ ...prev, api_allowed_roles: prev.api_allowed_roles.filter((_, i) => i !== index) }));
};
const addGroup = () => {
if (newGroup.trim()) {
setFormData(prev => ({ ...prev, api_allowed_groups: [...prev.api_allowed_groups, newGroup.trim()] }));
setNewGroup('');
}
};
const removeGroup = (index: number) => {
setFormData(prev => ({ ...prev, api_allowed_groups: prev.api_allowed_groups.filter((_, i) => i !== index) }));
};
const addServer = () => {
if (newServer.trim()) {
setFormData(prev => ({ ...prev, api_servers: [...prev.api_servers, newServer.trim()] }));
setNewServer('');
}
};
const removeServer = (index: number) => {
setFormData(prev => ({ ...prev, api_servers: prev.api_servers.filter((_, i) => i !== index) }));
};
const addHeader = () => {
if (newHeader.trim()) {
setFormData(prev => ({
...prev,
api_allowed_headers: [...(prev.api_allowed_headers || []), newHeader.trim()]
}));
setNewHeader('');
}
};
const removeHeader = (index: number) => {
setFormData(prev => ({
...prev,
api_allowed_headers: prev.api_allowed_headers?.filter((_, i) => i !== index) || []
}));
};
api_path: '',
api_description: '',
validation_enabled: false
})
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// Validate required fields
if (!formData.api_name || !formData.api_version || !formData.api_description) {
setError('Please fill in all required fields');
return;
}
e.preventDefault()
setLoading(true)
setError(null)
try {
setLoading(true);
setError(null);
const response = await fetch('http://localhost:3002/platform/api/', {
const response = await fetch('http://localhost:3002/platform/api', {
method: 'POST',
credentials: 'include',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Accept': 'application/json',
'Cookie': `access_token_cookie=${document.cookie.split('; ').find(row => row.startsWith('access_token_cookie='))?.split('=')[1]}`
},
body: JSON.stringify(formData)
});
})
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error_message || 'Failed to create API');
if (response.ok) {
router.push('/apis')
} else {
const errorData = await response.json()
setError(errorData.detail || 'Failed to create API')
}
// Redirect to APIs list after successful creation
router.push('/apis');
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to create API');
setError('Network error. Please try again.')
} finally {
setLoading(false);
setLoading(false)
}
};
}
const handleBack = () => {
router.back();
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
const { name, value, type } = e.target
setFormData(prev => ({
...prev,
[name]: type === 'checkbox' ? (e.target as HTMLInputElement).checked : value
}))
}
return (
<>
<div className="apis-topbar">
Doorman
</div>
<div className="apis-root">
<aside className="apis-sidebar">
<div className="apis-sidebar-title">Menu</div>
<ul className="apis-sidebar-list">
{menuItems.map((item, idx) => (
item.href ? (
<li key={item.label} className={`apis-sidebar-item${idx === 1 ? ' active' : ''}`}>
<Link href={item.href} style={{ color: 'inherit', textDecoration: 'none', display: 'block', width: '100%' }}>{item.label}</Link>
</li>
) : (
<li key={item.label} className={`apis-sidebar-item${idx === 1 ? ' active' : ''}`}>{item.label}</li>
)
))}
</ul>
<button className="apis-logout-btn" onClick={handleLogout}>
Logout
</button>
</aside>
<main className="apis-main">
<div className="apis-header-row">
<div className="add-api-header">
<button className="back-button" onClick={handleBack}>
<span className="back-arrow"></span>
Back to APIs
<Layout>
<div className="space-y-6">
{/* Page Header */}
<div className="page-header">
<div>
<h1 className="page-title">Add New API</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">
Create a new API endpoint for your gateway
</p>
</div>
<Link href="/apis" className="btn btn-secondary">
<svg className="h-4 w-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
Back to APIs
</Link>
</div>
{/* Error Message */}
{error && (
<div className="rounded-lg bg-error-50 border border-error-200 p-4 dark:bg-error-900/20 dark:border-error-800">
<div className="flex">
<svg className="h-5 w-5 text-error-400 dark:text-error-500 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div className="ml-3">
<p className="text-sm text-error-700 dark:text-error-300">{error}</p>
</div>
</div>
</div>
)}
{/* Form */}
<div className="card max-w-2xl">
<form onSubmit={handleSubmit} className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label htmlFor="api_name" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
API Name *
</label>
<input
id="api_name"
name="api_name"
type="text"
required
className="input"
placeholder="e.g., user-service"
value={formData.api_name}
onChange={handleChange}
disabled={loading}
/>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
A unique identifier for your API
</p>
</div>
<div>
<label htmlFor="api_version" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Version *
</label>
<input
id="api_version"
name="api_version"
type="text"
required
className="input"
placeholder="e.g., v1"
value={formData.api_version}
onChange={handleChange}
disabled={loading}
/>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
API version (e.g., v1, v2, beta)
</p>
</div>
</div>
<div>
<label htmlFor="api_type" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
API Type *
</label>
<select
id="api_type"
name="api_type"
required
className="input"
value={formData.api_type}
onChange={handleChange}
disabled={loading}
>
<option value="REST">REST</option>
<option value="GraphQL">GraphQL</option>
<option value="gRPC">gRPC</option>
<option value="SOAP">SOAP</option>
</select>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
The protocol type for this API
</p>
</div>
<div>
<label htmlFor="api_path" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
API Path *
</label>
<input
id="api_path"
name="api_path"
type="text"
required
className="input"
placeholder="e.g., https://api.example.com"
value={formData.api_path}
onChange={handleChange}
disabled={loading}
/>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
The base URL or path for your API
</p>
</div>
<div>
<label htmlFor="api_description" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Description
</label>
<textarea
id="api_description"
name="api_description"
rows={4}
className="input resize-none"
placeholder="Describe what this API does..."
value={formData.api_description}
onChange={handleChange}
disabled={loading}
/>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
Optional description of the API's purpose
</p>
</div>
<div className="flex items-center">
<input
id="validation_enabled"
name="validation_enabled"
type="checkbox"
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded"
checked={formData.validation_enabled}
onChange={handleChange}
disabled={loading}
/>
<label htmlFor="validation_enabled" className="ml-2 block text-sm text-gray-700 dark:text-gray-300">
Enable request validation
</label>
</div>
<div className="flex gap-4 pt-6 border-t border-gray-200 dark:border-gray-700">
<button
type="submit"
disabled={loading}
className="btn btn-primary flex-1"
>
{loading ? (
<div className="flex items-center justify-center">
<div className="spinner mr-2"></div>
Creating API...
</div>
) : (
'Create API'
)}
</button>
<h1 className="apis-title">Add New API</h1>
<Link href="/apis" className="btn btn-secondary flex-1">
Cancel
</Link>
</div>
</div>
{error && (
<div className="error-container">
<div className="error-message">
{error}
</div>
</div>
)}
<div className="add-api-content">
<form onSubmit={handleSubmit} className="add-api-form">
<div className="form-section">
<h2 className="section-title">Basic Information</h2>
<div className="form-grid">
<div className="form-group">
<label className="form-label">API Name *</label>
<input
type="text"
className="form-input"
value={formData.api_name}
onChange={(e) => handleInputChange('api_name', e.target.value)}
placeholder="Enter API name"
minLength={1}
maxLength={25}
required
/>
</div>
<div className="form-group">
<label className="form-label">API Version *</label>
<input
type="text"
className="form-input"
value={formData.api_version}
onChange={(e) => handleInputChange('api_version', e.target.value)}
placeholder="Enter API version"
minLength={1}
maxLength={8}
required
/>
</div>
<div className="form-group">
<label className="form-label">API Type</label>
<select
className="form-select"
value={formData.api_type}
onChange={(e) => handleInputChange('api_type', e.target.value)}
>
<option value="REST">REST</option>
</select>
</div>
<div className="form-group">
<label className="form-label">Retry Count</label>
<input
type="number"
className="form-input"
value={formData.api_allowed_retry_count}
onChange={(e) => handleInputChange('api_allowed_retry_count', parseInt(e.target.value) || 0)}
min="0"
placeholder="0"
/>
</div>
</div>
<div className="form-group">
<label className="form-label">Description *</label>
<textarea
className="form-textarea"
value={formData.api_description}
onChange={(e) => handleInputChange('api_description', e.target.value)}
placeholder="Enter API description"
minLength={1}
maxLength={127}
required
rows={3}
/>
</div>
</div>
<div className="form-section">
<h2 className="section-title">Allowed Roles</h2>
<div className="groups-container">
<div className="groups-list">
{formData.api_allowed_roles.map((role, index) => (
<span key={index} className="group-tag">
{role}
<button
type="button"
className="remove-group-btn"
onClick={() => removeRole(index)}
>
×
</button>
</span>
))}
</div>
<div className="add-group">
<input
type="text"
className="form-input"
placeholder="Enter role name"
value={newRole}
onChange={(e) => setNewRole(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && (e.preventDefault(), addRole())}
/>
<button type="button" className="add-button" onClick={addRole}>
Add Role
</button>
</div>
</div>
</div>
<div className="form-section">
<h2 className="section-title">Allowed Groups</h2>
<div className="groups-container">
<div className="groups-list">
{formData.api_allowed_groups.map((group, index) => (
<span key={index} className="group-tag">
{group}
<button
type="button"
className="remove-group-btn"
onClick={() => removeGroup(index)}
>
×
</button>
</span>
))}
</div>
<div className="add-group">
<input
type="text"
className="form-input"
placeholder="Enter group name"
value={newGroup}
onChange={(e) => setNewGroup(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && (e.preventDefault(), addGroup())}
/>
<button type="button" className="add-button" onClick={addGroup}>
Add Group
</button>
</div>
</div>
</div>
<div className="form-section">
<h2 className="section-title">Backend Servers</h2>
<div className="groups-container">
<div className="groups-list">
{formData.api_servers.map((server, index) => (
<span key={index} className="group-tag">
{server}
<button
type="button"
className="remove-group-btn"
onClick={() => removeServer(index)}
>
×
</button>
</span>
))}
</div>
<div className="add-group">
<input
type="text"
className="form-input"
placeholder="Enter server URL (e.g., http://localhost:8080)"
value={newServer}
onChange={(e) => setNewServer(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && (e.preventDefault(), addServer())}
/>
<button type="button" className="add-button" onClick={addServer}>
Add Server
</button>
</div>
</div>
</div>
<div className="form-section">
<h2 className="section-title">Authorization & Headers</h2>
<div className="form-grid">
<div className="form-group">
<label className="form-label">Authorization Field Swap</label>
<input
type="text"
className="form-input"
value={formData.api_authorization_field_swap || ''}
onChange={(e) => handleInputChange('api_authorization_field_swap', e.target.value)}
placeholder="e.g., backend-auth-header"
/>
</div>
<div className="form-group">
<label className="form-label">Token Group</label>
<input
type="text"
className="form-input"
value={formData.api_token_group || ''}
onChange={(e) => handleInputChange('api_token_group', e.target.value)}
placeholder="e.g., ai-group-1"
/>
</div>
<div className="form-group">
<label className="form-label">Tokens Enabled</label>
<select
className="form-select"
value={formData.api_tokens_enabled ? 'true' : 'false'}
onChange={(e) => handleInputChange('api_tokens_enabled', e.target.value === 'true')}
>
<option value="false">Disabled</option>
<option value="true">Enabled</option>
</select>
</div>
</div>
</div>
<div className="form-section">
<h2 className="section-title">Allowed Headers</h2>
<div className="groups-container">
<div className="groups-list">
{(formData.api_allowed_headers || []).map((header, index) => (
<span key={index} className="group-tag">
{header}
<button
type="button"
className="remove-group-btn"
onClick={() => removeHeader(index)}
>
×
</button>
</span>
))}
</div>
<div className="add-group">
<input
type="text"
className="form-input"
placeholder="Enter header name (e.g., Content-Type)"
value={newHeader}
onChange={(e) => setNewHeader(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && (e.preventDefault(), addHeader())}
/>
<button type="button" className="add-button" onClick={addHeader}>
Add Header
</button>
</div>
</div>
</div>
<div className="form-actions">
<button type="button" className="cancel-button" onClick={handleBack}>
Cancel
</button>
<button type="submit" className="save-button" disabled={loading}>
{loading ? 'Creating API...' : 'Create API'}
</button>
</div>
</form>
</div>
</main>
</form>
</div>
</div>
</>
);
};
</Layout>
)
}
export default AddApiPage;
export default AddApiPage
+196 -172
View File
@@ -1,71 +1,43 @@
'use client'
import React, { useState, useEffect, Key, ReactNode } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import './apis.css';
import React, { useState, useEffect } from 'react'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
import Layout from '@/components/Layout'
interface API {
api_version: ReactNode;
api_type: ReactNode;
api_description: ReactNode;
api_path: ReactNode;
api_id: Key | null | undefined;
api_name: ReactNode;
id: string;
name: string;
version: string;
description: string;
status: string;
endpoints: number;
lastUpdated: string;
api_version: React.ReactNode
api_type: React.ReactNode
api_description: React.ReactNode
api_path: React.ReactNode
api_id: React.ReactNode
api_name: React.ReactNode
id: string
name: string
version: string
description: string
status: string
endpoints: number
lastUpdated: string
}
const menuItems = [
{ label: 'Dashboard', href: '/dashboard' },
{ label: 'APIs', href: '/apis' },
{ label: 'Routings', href: '/routings' },
{ label: 'Users', href: '/users' },
{ label: 'Groups', href: '/groups' },
{ label: 'Roles', href: '/roles' },
{ label: 'Monitor', href: '/monitor' },
{ label: 'Logs', href: '/logging' },
{ label: 'Security', href: '/security' },
{ label: 'Settings', href: '/settings' },
];
const handleLogout = () => {
localStorage.clear();
sessionStorage.clear();
setTimeout(() => {
window.location.replace('/');
}, 50);
};
const APIsPage = () => {
const router = useRouter();
const [theme, setTheme] = useState('light');
const [apis, setApis] = useState<API[]>([]);
const [allApis, setAllApis] = useState<API[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [searchTerm, setSearchTerm] = useState('');
const [sortBy, setSortBy] = useState('name');
const router = useRouter()
const [apis, setApis] = useState<API[]>([])
const [allApis, setAllApis] = useState<API[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [searchTerm, setSearchTerm] = useState('')
const [sortBy, setSortBy] = useState('name')
useEffect(() => {
const savedTheme = localStorage.getItem('theme') || 'light';
setTheme(savedTheme);
document.documentElement.setAttribute('data-theme', savedTheme);
}, []);
useEffect(() => {
fetchApis();
}, []);
fetchApis()
}, [])
const fetchApis = async () => {
try {
setLoading(true);
setError(null);
setLoading(true)
setError(null)
const response = await fetch(`http://localhost:3002/platform/api/all`, {
credentials: 'include',
headers: {
@@ -73,28 +45,28 @@ const APIsPage = () => {
'Content-Type': 'application/json',
'Cookie': `access_token_cookie=${document.cookie.split('; ').find(row => row.startsWith('access_token_cookie='))?.split('=')[1]}`
}
});
})
if (!response.ok) {
throw new Error('Failed to load APIs');
throw new Error('Failed to load APIs')
}
const data = await response.json();
const apiList = Array.isArray(data) ? data : (data.apis || data.response?.apis || []);
setAllApis(apiList);
setApis(apiList);
const data = await response.json()
const apiList = Array.isArray(data) ? data : (data.apis || data.response?.apis || [])
setAllApis(apiList)
setApis(apiList)
} catch (err) {
setError('Failed to load APIs. Please try again later.');
setApis([]);
setAllApis([]);
setError('Failed to load APIs. Please try again later.')
setApis([])
setAllApis([])
} finally {
setLoading(false);
setLoading(false)
}
};
}
const handleSearch = (e: React.FormEvent) => {
e.preventDefault();
e.preventDefault()
if (!searchTerm.trim()) {
setApis(allApis);
return;
setApis(allApis)
return
}
const filteredApis = allApis.filter(api =>
@@ -103,124 +75,119 @@ const APIsPage = () => {
(api.api_type as string)?.toLowerCase().includes(searchTerm.toLowerCase()) ||
(api.api_path as string)?.toLowerCase().includes(searchTerm.toLowerCase()) ||
(api.api_description as string)?.toLowerCase().includes(searchTerm.toLowerCase())
);
setApis(filteredApis);
};
)
setApis(filteredApis)
}
const handleSort = (sortField: string) => {
setSortBy(sortField);
setSortBy(sortField)
const sortedApis = [...apis].sort((a, b) => {
if (sortField === 'api_name') {
return (a.api_name as string).localeCompare(b.api_name as string);
return (a.api_name as string).localeCompare(b.api_name as string)
} else if (sortField === 'api_version') {
return (a.api_version as string).localeCompare(b.api_version as string);
return (a.api_version as string).localeCompare(b.api_version as string)
} else if (sortField === 'api_type') {
return (a.api_type as string).localeCompare(b.api_type as string);
return (a.api_type as string).localeCompare(b.api_type as string)
}
return 0;
});
setApis(sortedApis);
};
return 0
})
setApis(sortedApis)
}
const handleApiClick = (api: API) => {
sessionStorage.setItem('selectedApi', JSON.stringify(api));
router.push(`/apis/${api.api_id}`);
};
sessionStorage.setItem('selectedApi', JSON.stringify(api))
router.push(`/apis/${api.api_id}`)
}
return (
<>
<div className="apis-topbar">
Doorman
<button
className="apis-refresh-btn"
onClick={fetchApis}
aria-label="Refresh APIs"
>
<span className="refresh-icon"></span>
</button>
</div>
<div className="apis-root">
<aside className="apis-sidebar">
<div className="apis-sidebar-title">Menu</div>
<ul className="apis-sidebar-list">
{menuItems.map((item) => (
item.href ? (
<li key={`menu-${item.label}`} className={`apis-sidebar-item${item.label === 'APIs' ? ' active' : ''}`}>
<Link href={item.href} style={{ color: 'inherit', textDecoration: 'none', display: 'block', width: '100%' }}>{item.label}</Link>
</li>
) : (
<li key={`menu-${item.label}`} className={`apis-sidebar-item${item.label === 'APIs' ? ' active' : ''}`}>{item.label}</li>
)
))}
</ul>
<button className="apis-logout-btn" onClick={handleLogout}>
Logout
</button>
</aside>
<main className="apis-main">
<div className="apis-header-row">
<h1 className="apis-title">APIs</h1>
<button className="refresh-button" onClick={fetchApis}>
<span className="refresh-icon"></span>
Refresh
</button>
<Layout>
<div className="space-y-6">
{/* Page Header */}
<div className="page-header">
<div>
<h1 className="page-title">APIs</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">
Manage and monitor your API endpoints
</p>
</div>
<Link href="/apis/add" className="btn btn-primary">
<svg className="h-4 w-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
Add API
</Link>
</div>
<div className="apis-controls">
<form className="apis-search-box" onSubmit={handleSearch}>
<input
type="text"
className="apis-search-input"
placeholder="Search APIs..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
<button type="submit" className="apis-search-btn">Search</button>
<Link href="/apis/add" className="apis-add-btn" style={{ textDecoration: 'none', display: 'inline-block' }}>
Add API
</Link>
{/* Search and Filters */}
<div className="card">
<div className="flex flex-col sm:flex-row gap-4">
<form onSubmit={handleSearch} className="flex-1">
<div className="relative">
<svg className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
<input
type="text"
className="search-input"
placeholder="Search APIs by name, version, type, or description..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
</form>
<div className="apis-sort-group">
<button
key="sort-name"
className={`apis-sort-btn ${sortBy === 'api_name' ? 'active' : ''}`}
<div className="flex gap-2">
<button
onClick={() => handleSort('api_name')}
className={`btn ${sortBy === 'api_name' ? 'btn-primary' : 'btn-secondary'}`}
>
Name
</button>
<button
key="sort-version"
className={`apis-sort-btn ${sortBy === 'api_version' ? 'active' : ''}`}
<button
onClick={() => handleSort('api_version')}
className={`btn ${sortBy === 'api_version' ? 'btn-primary' : 'btn-secondary'}`}
>
Version
</button>
<button
key="sort-type"
className={`apis-sort-btn ${sortBy === 'api_type' ? 'active' : ''}`}
<button
onClick={() => handleSort('api_type')}
className={`btn ${sortBy === 'api_type' ? 'btn-primary' : 'btn-secondary'}`}
>
Status
Type
</button>
</div>
</div>
</div>
{error && (
<div className="error-container">
<div className="error-message">
{error}
{/* Error Message */}
{error && (
<div className="rounded-lg bg-error-50 border border-error-200 p-4 dark:bg-error-900/20 dark:border-error-800">
<div className="flex">
<svg className="h-5 w-5 text-error-400 dark:text-error-500 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div className="ml-3">
<p className="text-sm text-error-700 dark:text-error-300">{error}</p>
</div>
</div>
)}
</div>
)}
{loading ? (
<div className="loading-spinner">
<div className="spinner"></div>
<p>Loading APIs...</p>
{/* Loading State */}
{loading ? (
<div className="card">
<div className="flex items-center justify-center py-12">
<div className="text-center">
<div className="spinner mx-auto mb-4"></div>
<p className="text-gray-600 dark:text-gray-400">Loading APIs...</p>
</div>
</div>
) : (
<div className="apis-table-panel">
<table className="apis-table">
</div>
) : (
/* APIs Table */
<div className="card">
<div className="overflow-x-auto">
<table className="table">
<thead>
<tr>
<th>Name</th>
@@ -228,34 +195,91 @@ const APIsPage = () => {
<th>Path</th>
<th>Description</th>
<th>Type</th>
<th></th>
</tr>
</thead>
<tbody>
{apis.map((api, index) => (
<tr
key={api.api_id || `${api.api_name}-${api.api_version}-${index}`}
key={String(api.api_id) || `${api.api_name}-${api.api_version}-${index}`}
onClick={() => handleApiClick(api)}
style={{ cursor: 'pointer' }}
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#f5f5f5'}
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = ''}
className="cursor-pointer hover:bg-gray-50 dark:hover:bg-dark-surfaceHover transition-colors"
>
<td>{api.api_name}</td>
<td>
<span className="apis-version-badge">{api.api_version}</span>
<div className="flex items-center">
<div className="h-8 w-8 rounded-lg bg-primary-100 dark:bg-primary-900/20 flex items-center justify-center mr-3">
<svg className="h-4 w-4 text-primary-600 dark:text-primary-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</div>
<div>
<p className="font-medium text-gray-900 dark:text-white">{api.api_name}</p>
</div>
</div>
</td>
<td>
<span className="badge badge-primary">{api.api_version}</span>
</td>
<td>
<code className="text-sm bg-gray-100 dark:bg-gray-800 px-2 py-1 rounded">
{api.api_path}
</code>
</td>
<td>
<p className="text-sm text-gray-600 dark:text-gray-400 max-w-xs truncate">
{api.api_description}
</p>
</td>
<td>
<span className={`badge ${
(api.api_type as string)?.toLowerCase() === 'rest' ? 'badge-success' :
(api.api_type as string)?.toLowerCase() === 'graphql' ? 'badge-warning' :
(api.api_type as string)?.toLowerCase() === 'grpc' ? 'badge-error' :
'badge-gray'
}`}>
{api.api_type}
</span>
</td>
<td>
<button className="btn btn-ghost btn-sm">
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</button>
</td>
<td>{api.api_path}</td>
<td>{api.api_description}</td>
<td>{api.api_type}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</main>
</div>
</>
);
};
export default APIsPage;
{/* Empty State */}
{apis.length === 0 && !loading && (
<div className="text-center py-12">
<div className="h-16 w-16 mx-auto mb-4 rounded-full bg-gray-100 dark:bg-gray-800 flex items-center justify-center">
<svg className="h-8 w-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</div>
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">No APIs found</h3>
<p className="text-gray-600 dark:text-gray-400 mb-4">
{searchTerm ? 'Try adjusting your search terms.' : 'Get started by creating your first API.'}
</p>
<Link href="/apis/add" className="btn btn-primary">
<svg className="h-4 w-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
Add API
</Link>
</div>
)}
</div>
)}
</div>
</Layout>
)
}
export default APIsPage
+188 -152
View File
@@ -1,53 +1,31 @@
'use client'
import React, { useState, useEffect } from 'react';
import './dashboard.css';
import React, { useState, useEffect } from 'react'
import Layout from '@/components/Layout'
interface DashboardData {
totalRequests: number;
activeUsers: number;
newApis: number;
totalRequests: number
activeUsers: number
newApis: number
monthlyUsage: {
[key: string]: number;
};
[key: string]: number
}
activeUsersList: Array<{
name: string;
email: string;
}>;
name: string
email: string
}>
popularApis: Array<{
id: string;
name: string;
version: string;
requests: string;
subscribers: number;
}>;
id: string
name: string
version: string
requests: string
subscribers: number
}>
}
const menuItems = [
{ label: 'Dashboard', href: '/dashboard' },
{ label: 'APIs', href: '/apis' },
{ label: 'Routings', href: '/routings' },
{ label: 'Users', href: '/users' },
{ label: 'Groups', href: '/groups' },
{ label: 'Roles', href: '/roles' },
{ label: 'Monitor', href: '/monitor' },
{ label: 'Logs', href: '/logging' },
{ label: 'Security', href: '/security' },
{ label: 'Settings', href: '/settings' },
];
const handleLogout = () => {
localStorage.clear();
sessionStorage.clear();
setTimeout(() => {
window.location.replace('/');
}, 50);
};
const Dashboard = () => {
const [theme, setTheme] = useState('light');
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [dashboardData, setDashboardData] = useState<DashboardData>({
totalRequests: 0,
activeUsers: 0,
@@ -55,135 +33,185 @@ const Dashboard = () => {
monthlyUsage: {},
activeUsersList: [],
popularApis: []
});
})
useEffect(() => {
const savedTheme = localStorage.getItem('theme') || 'light';
setTheme(savedTheme);
document.documentElement.setAttribute('data-theme', savedTheme);
}, []);
useEffect(() => {
fetchData();
}, []);
fetchData()
}, [])
const fetchData = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch(`${process.env.NEXT_PUBLIC_SERVER_URL}/platform/dashboard`);
setLoading(true)
setError(null)
const response = await fetch(`${process.env.NEXT_PUBLIC_SERVER_URL || 'http://localhost:3002'}/platform/dashboard`, {
credentials: 'include',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Cookie': `access_token_cookie=${document.cookie.split('; ').find(row => row.startsWith('access_token_cookie='))?.split('=')[1]}`
}
})
if (!response.ok) {
throw new Error('Failed to fetch dashboard data');
throw new Error('Failed to fetch dashboard data')
}
const data = await response.json();
setDashboardData(data);
const data = await response.json()
setDashboardData(data)
} catch (err) {
if (err instanceof Error) {
setError(err.message);
setError(err.message)
} else {
setError('An unknown error occurred');
setError('An unknown error occurred')
}
} finally {
setLoading(false);
setLoading(false)
}
};
}
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
return (
<>
<div className="dashboard-topbar">
Doorman
</div>
<div className="dashboard-root">
<aside className="sidebar">
<div className="sidebar-title">Menu</div>
<ul className="sidebar-list">
{menuItems.map((item, idx) => (
item.href ? (
<li key={item.label} className={`sidebar-item${idx === 0 ? ' active' : ''}`}>
<a href={item.href} style={{ color: 'inherit', textDecoration: 'none', display: 'block', width: '100%' }}>{item.label}</a>
</li>
) : (
<li key={item.label} className={`sidebar-item${idx === 0 ? ' active' : ''}`}>{item.label}</li>
)
))}
</ul>
<button className="logout-btn" onClick={handleLogout}>
Logout
<Layout>
<div className="space-y-6">
{/* Page Header */}
<div className="page-header">
<div>
<h1 className="page-title">Dashboard</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">
Overview of your API gateway performance and usage
</p>
</div>
<button
onClick={fetchData}
disabled={loading}
className="btn btn-secondary"
>
<svg className="h-4 w-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
{loading ? 'Refreshing...' : 'Refresh'}
</button>
</aside>
<main className="dashboard-main">
<div className="dashboard-header">
<h1>Dashboard</h1>
<button className="refresh-button" onClick={fetchData}>
<span className="refresh-icon"></span>
Refresh
</button>
</div>
</div>
{error && (
<div className="error-container">
<div className="error-message">
<span className="error-icon"></span>
{error}
{/* Error Message */}
{error && (
<div className="rounded-lg bg-error-50 border border-error-200 p-4 dark:bg-error-900/20 dark:border-error-800">
<div className="flex">
<svg className="h-5 w-5 text-error-400 dark:text-error-500 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div className="ml-3">
<p className="text-sm text-error-700 dark:text-error-300">{error}</p>
</div>
</div>
)}
</div>
)}
<div className="dashboard-cards-row">
<div className="dashboard-card">
<div className="dashboard-card-title">Total Monthly Requests</div>
<div className="dashboard-card-value">{dashboardData.totalRequests.toLocaleString()}</div>
<div className="dashboard-card-sub">+17% this month</div>
</div>
<div className="dashboard-card">
<div className="dashboard-card-title">Active Monthly Users</div>
<div className="dashboard-card-value">{dashboardData.activeUsers}</div>
<div className="dashboard-card-sub">+4% this month</div>
</div>
<div className="dashboard-card">
<div className="dashboard-card-title">New APIs This Month</div>
<div className="dashboard-card-value">{dashboardData.newApis}</div>
<div className="dashboard-card-sub">+25% this month</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div className="stats-card">
<div className="flex items-center justify-between">
<div>
<p className="stats-label">Total Monthly Requests</p>
<p className="stats-value">{dashboardData.totalRequests.toLocaleString()}</p>
<p className="stats-change positive">+17% this month</p>
</div>
<div className="h-12 w-12 rounded-lg bg-primary-100 dark:bg-primary-900/20 flex items-center justify-center">
<svg className="h-6 w-6 text-primary-600 dark:text-primary-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" />
</svg>
</div>
</div>
</div>
<div className="dashboard-row">
<div className="dashboard-panel dashboard-usage">
<div className="dashboard-panel-title">Monthly Usage</div>
<div className="dashboard-bar-chart">
{['Jan','Feb','Mar','Apr','May','June','July','Aug','Sept','Oct','Nov','Dec'].map((m, i) => (
<div key={m} className="bar-group">
<div
className="bar"
style={{
height: `${dashboardData.monthlyUsage[m] || 0}%`
}}
></div>
<div className="bar-label">{m}</div>
<div className="stats-card">
<div className="flex items-center justify-between">
<div>
<p className="stats-label">Active Monthly Users</p>
<p className="stats-value">{dashboardData.activeUsers}</p>
<p className="stats-change positive">+4% this month</p>
</div>
<div className="h-12 w-12 rounded-lg bg-success-100 dark:bg-success-900/20 flex items-center justify-center">
<svg className="h-6 w-6 text-success-600 dark:text-success-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z" />
</svg>
</div>
</div>
</div>
<div className="stats-card">
<div className="flex items-center justify-between">
<div>
<p className="stats-label">New APIs This Month</p>
<p className="stats-value">{dashboardData.newApis}</p>
<p className="stats-change positive">+25% this month</p>
</div>
<div className="h-12 w-12 rounded-lg bg-warning-100 dark:bg-warning-900/20 flex items-center justify-center">
<svg className="h-6 w-6 text-warning-600 dark:text-warning-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</div>
</div>
</div>
</div>
{/* Charts and Lists */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Monthly Usage Chart */}
<div className="lg:col-span-2 card">
<div className="card-header">
<h3 className="card-title">Monthly Usage</h3>
</div>
<div className="h-64 flex items-end justify-between gap-2">
{months.map((month) => (
<div key={month} className="flex-1 flex flex-col items-center">
<div
className="w-full bg-gradient-to-t from-primary-500 to-primary-600 rounded-t-lg transition-all duration-300 hover:from-primary-600 hover:to-primary-700"
style={{
height: `${Math.max((dashboardData.monthlyUsage[month] || 0) * 2, 4)}px`
}}
></div>
<span className="text-xs text-gray-500 dark:text-gray-400 mt-2">{month}</span>
</div>
))}
</div>
</div>
{/* Active Users */}
<div className="card">
<div className="card-header">
<h3 className="card-title">Most Active Users</h3>
</div>
<div className="space-y-4">
{dashboardData.activeUsersList.map((user, index) => (
<div key={user.email} className="flex items-center space-x-3">
<div className="h-8 w-8 rounded-full bg-gradient-to-br from-primary-400 to-primary-600 flex items-center justify-center text-white text-sm font-medium">
{user.name.charAt(0).toUpperCase()}
</div>
))}
</div>
</div>
<div className="dashboard-panel dashboard-users">
<div className="dashboard-panel-title">Most Active Users</div>
<ul className="dashboard-user-list">
{dashboardData.activeUsersList.map(u => (
<li key={u.email} className="dashboard-user-item">
<div className="user-avatar"></div>
<div>
<div className="user-name">{u.name}</div>
<div className="user-email">{u.email}</div>
</div>
</li>
))}
</ul>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-900 dark:text-white truncate">
{user.name}
</p>
<p className="text-xs text-gray-500 dark:text-gray-400 truncate">
{user.email}
</p>
</div>
</div>
))}
</div>
</div>
<div className="dashboard-panel dashboard-table">
<div className="dashboard-panel-title">Popular APIs This Month</div>
<table className="dashboard-table-main">
</div>
{/* Popular APIs Table */}
<div className="card">
<div className="card-header">
<h3 className="card-title">Popular APIs This Month</h3>
</div>
<div className="overflow-x-auto">
<table className="table">
<thead>
<tr>
<th>Id</th>
<th>ID</th>
<th>Name</th>
<th>Version</th>
<th>Requests</th>
@@ -192,23 +220,31 @@ const Dashboard = () => {
</tr>
</thead>
<tbody>
{dashboardData.popularApis.map(api => (
{dashboardData.popularApis.map((api) => (
<tr key={api.id}>
<td>{api.id}</td>
<td>{api.name}</td>
<td>{api.version}</td>
<td className="font-mono text-sm">{api.id}</td>
<td className="font-medium">{api.name}</td>
<td>
<span className="badge badge-primary">{api.version}</span>
</td>
<td>{api.requests}</td>
<td>{api.subscribers}</td>
<td>...</td>
<td>
<button className="btn btn-ghost btn-sm">
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 5v.01M12 12v.01M12 19v.01M12 6a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2z" />
</svg>
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</main>
</div>
</div>
</>
);
};
</Layout>
)
}
export default Dashboard;
export default Dashboard
+182
View File
@@ -0,0 +1,182 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
* {
@apply border-gray-200 dark:border-gray-700;
}
body {
@apply bg-gray-50 dark:bg-dark-bg text-gray-900 dark:text-dark-text;
}
}
@layer components {
/* Button variants */
.btn {
@apply inline-flex items-center justify-center rounded-lg px-4 py-2 text-sm font-medium transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed;
}
.btn-primary {
@apply bg-primary-600 text-white hover:bg-primary-700 focus:ring-primary-500 shadow-sm;
}
.btn-secondary {
@apply bg-gray-100 text-gray-900 hover:bg-gray-200 focus:ring-gray-500 dark:bg-dark-surface dark:text-dark-text dark:hover:bg-dark-surfaceHover;
}
.btn-ghost {
@apply bg-transparent text-gray-700 hover:bg-gray-100 focus:ring-gray-500 dark:text-dark-textSecondary dark:hover:bg-dark-surfaceHover;
}
.btn-danger {
@apply bg-error-600 text-white hover:bg-error-700 focus:ring-error-500;
}
/* Input styles */
.input {
@apply w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm placeholder-gray-500 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500 dark:border-dark-border dark:bg-dark-surface dark:text-dark-text dark:placeholder-gray-400 dark:focus:border-primary-400 dark:focus:ring-primary-400;
}
/* Card styles */
.card {
@apply rounded-xl bg-white p-6 shadow-soft dark:bg-dark-surface dark:shadow-none dark:border dark:border-dark-border;
}
.card-header {
@apply mb-4 flex items-center justify-between;
}
.card-title {
@apply text-lg font-semibold text-gray-900 dark:text-dark-text;
}
/* Table styles */
.table {
@apply w-full border-collapse;
}
.table th {
@apply border-b border-gray-200 bg-gray-50 px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:border-dark-border dark:bg-dark-surface dark:text-dark-textSecondary;
}
.table td {
@apply border-b border-gray-100 px-4 py-4 text-sm text-gray-900 dark:border-dark-border dark:text-dark-text;
}
.table tbody tr {
@apply hover:bg-gray-50 dark:hover:bg-dark-surfaceHover transition-colors duration-150;
}
/* Badge styles */
.badge {
@apply inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium;
}
.badge-primary {
@apply bg-primary-100 text-primary-800 dark:bg-primary-900 dark:text-primary-200;
}
.badge-success {
@apply bg-success-100 text-success-800 dark:bg-success-900 dark:text-success-200;
}
.badge-warning {
@apply bg-warning-100 text-warning-800 dark:bg-warning-900 dark:text-warning-200;
}
.badge-error {
@apply bg-error-100 text-error-800 dark:bg-error-900 dark:text-error-200;
}
.badge-gray {
@apply bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200;
}
/* Loading spinner */
.spinner {
@apply animate-spin rounded-full border-2 border-gray-300 border-t-primary-600 h-6 w-6;
}
/* Sidebar styles */
.sidebar {
@apply fixed left-0 top-0 z-40 h-screen w-64 -translate-x-full transform bg-white shadow-strong transition-transform duration-300 dark:bg-dark-surface dark:border-r dark:border-dark-border lg:translate-x-0;
}
.sidebar-item {
@apply flex items-center rounded-lg px-3 py-2 text-sm font-medium text-gray-700 transition-colors duration-200 hover:bg-gray-100 hover:text-gray-900 dark:text-dark-textSecondary dark:hover:bg-dark-surfaceHover dark:hover:text-dark-text;
}
.sidebar-item.active {
@apply bg-primary-50 text-primary-700 dark:bg-primary-900/20 dark:text-primary-400;
}
/* Topbar styles */
.topbar {
@apply fixed top-0 z-30 h-16 w-full bg-white/80 backdrop-blur-sm border-b border-gray-200 dark:bg-dark-surface/80 dark:border-dark-border;
}
/* Main content area */
.main-content {
@apply lg:ml-64 pt-16 min-h-screen bg-gray-50 dark:bg-dark-bg;
}
/* Page header */
.page-header {
@apply mb-6 flex items-center justify-between;
}
.page-title {
@apply text-2xl font-bold text-gray-900 dark:text-dark-text;
}
/* Search input */
.search-input {
@apply w-full max-w-md rounded-lg border border-gray-300 bg-white pl-10 pr-4 py-2 text-sm placeholder-gray-500 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500 dark:border-dark-border dark:bg-dark-surface dark:text-dark-text dark:placeholder-gray-400;
}
/* Stats cards */
.stats-card {
@apply rounded-xl bg-white p-6 shadow-soft dark:bg-dark-surface dark:border dark:border-dark-border;
}
.stats-value {
@apply text-3xl font-bold text-gray-900 dark:text-dark-text;
}
.stats-label {
@apply text-sm font-medium text-gray-500 dark:text-dark-textSecondary;
}
.stats-change {
@apply text-sm font-medium;
}
.stats-change.positive {
@apply text-success-600 dark:text-success-400;
}
.stats-change.negative {
@apply text-error-600 dark:text-error-400;
}
}
@layer utilities {
.text-balance {
text-wrap: balance;
}
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
.gradient-text {
@apply bg-gradient-to-r from-primary-600 to-primary-800 bg-clip-text text-transparent;
}
}
+346 -318
View File
@@ -1,77 +1,51 @@
'use client'
import React, { useState, useEffect } from 'react';
import Link from 'next/link';
import { useRouter, useParams } from 'next/navigation';
import '../groups.css';
import React, { useState, useEffect } from 'react'
import Link from 'next/link'
import { useRouter, useParams } from 'next/navigation'
import Layout from '@/components/Layout'
interface Group {
group_name: string;
group_description: string;
api_access?: string[];
group_name: string
group_description: string
api_access?: string[]
}
const menuItems = [
{ label: 'Dashboard', href: '/dashboard' },
{ label: 'APIs', href: '/apis' },
{ label: 'Routings', href: '/routings' },
{ label: 'Users', href: '/users' },
{ label: 'Groups', href: '/groups' },
{ label: 'Roles', href: '/roles' },
{ label: 'Monitor', href: '/monitor' },
{ label: 'Logs', href: '/logging' },
{ label: 'Security', href: '/security' },
{ label: 'Settings', href: '/settings' },
];
const handleLogout = () => {
localStorage.clear();
sessionStorage.clear();
setTimeout(() => {
window.location.replace('/');
}, 50);
};
const GroupDetailPage = () => {
const router = useRouter();
const params = useParams();
const groupName = params.groupName as string;
const router = useRouter()
const params = useParams()
const groupName = params.groupName as string
const [theme, setTheme] = useState('light');
const [group, setGroup] = useState<Group | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [isEditing, setIsEditing] = useState(false);
const [editData, setEditData] = useState<Partial<Group>>({});
const [saving, setSaving] = useState(false);
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [deleting, setDeleting] = useState(false);
const [deleteConfirmation, setDeleteConfirmation] = useState('');
const [group, setGroup] = useState<Group | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [success, setSuccess] = useState<string | null>(null)
const [isEditing, setIsEditing] = useState(false)
const [editData, setEditData] = useState<Partial<Group>>({})
const [saving, setSaving] = useState(false)
const [showDeleteModal, setShowDeleteModal] = useState(false)
const [deleting, setDeleting] = useState(false)
const [deleteConfirmation, setDeleteConfirmation] = useState('')
const [newApi, setNewApi] = useState('')
useEffect(() => {
const savedTheme = localStorage.getItem('theme') || 'light';
setTheme(savedTheme);
document.documentElement.setAttribute('data-theme', savedTheme);
}, []);
useEffect(() => {
fetchGroup();
}, [groupName]);
fetchGroup()
}, [groupName])
const fetchGroup = async () => {
try {
setLoading(true);
setError(null);
setLoading(true)
setError(null)
// Try to get from sessionStorage first
const savedGroup = sessionStorage.getItem('selectedGroup');
const savedGroup = sessionStorage.getItem('selectedGroup')
if (savedGroup) {
const parsedGroup = JSON.parse(savedGroup);
const parsedGroup = JSON.parse(savedGroup)
if (parsedGroup.group_name === groupName) {
setGroup(parsedGroup);
setEditData(parsedGroup);
setLoading(false);
return;
setGroup(parsedGroup)
setEditData(parsedGroup)
setLoading(false)
return
}
}
@@ -83,35 +57,41 @@ const GroupDetailPage = () => {
'Content-Type': 'application/json',
'Cookie': `access_token_cookie=${document.cookie.split('; ').find(row => row.startsWith('access_token_cookie='))?.split('=')[1]}`
}
});
})
if (!response.ok) {
throw new Error('Failed to load group');
throw new Error('Failed to load group')
}
const data = await response.json();
setGroup(data);
setEditData(data);
const data = await response.json()
setGroup(data)
setEditData(data)
} catch (err) {
setError('Failed to load group. Please try again later.');
setError('Failed to load group. Please try again later.')
} finally {
setLoading(false);
setLoading(false)
}
};
}
const handleBack = () => {
router.push('/groups')
}
const handleEdit = () => {
setIsEditing(true);
};
setIsEditing(true)
}
const handleCancel = () => {
setIsEditing(false);
setEditData(group || {});
};
setIsEditing(false)
if (group) {
setEditData(group)
}
}
const handleSave = async () => {
try {
setSaving(true);
setError(null);
setSaving(true)
setError(null)
const response = await fetch(`http://localhost:3002/platform/group/${encodeURIComponent(groupName)}`, {
method: 'PUT',
@@ -122,26 +102,38 @@ const GroupDetailPage = () => {
'Cookie': `access_token_cookie=${document.cookie.split('; ').find(row => row.startsWith('access_token_cookie='))?.split('=')[1]}`
},
body: JSON.stringify(editData)
});
})
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error_message || 'Failed to update group');
const errorData = await response.json()
throw new Error(errorData.detail || 'Failed to update group')
}
setGroup({ ...group, ...editData } as Group);
setIsEditing(false);
const updatedGroup = await response.json()
setGroup(updatedGroup)
setIsEditing(false)
setSuccess('Group updated successfully!')
setTimeout(() => setSuccess(null), 3000)
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to update group. Please try again later.');
if (err instanceof Error) {
setError(err.message)
} else {
setError('Failed to update group. Please try again.')
}
} finally {
setSaving(false);
setSaving(false)
}
};
}
const handleDelete = async () => {
if (deleteConfirmation !== group?.group_name) {
setError('Group name does not match')
return
}
try {
setDeleting(true);
setError(null);
setDeleting(true)
setError(null)
const response = await fetch(`http://localhost:3002/platform/group/${encodeURIComponent(groupName)}`, {
method: 'DELETE',
@@ -151,295 +143,331 @@ const GroupDetailPage = () => {
'Content-Type': 'application/json',
'Cookie': `access_token_cookie=${document.cookie.split('; ').find(row => row.startsWith('access_token_cookie='))?.split('=')[1]}`
}
});
})
if (!response.ok) {
throw new Error('Failed to delete group');
const errorData = await response.json()
throw new Error(errorData.detail || 'Failed to delete group')
}
router.push('/groups');
router.push('/groups')
} catch (err) {
setError('Failed to delete group. Please try again later.');
setShowDeleteModal(false);
if (err instanceof Error) {
setError(err.message)
} else {
setError('Failed to delete group. Please try again.')
}
} finally {
setDeleting(false);
setDeleting(false)
setShowDeleteModal(false)
}
};
}
const handleInputChange = (field: keyof Group, value: any) => {
setEditData(prev => ({ ...prev, [field]: value }));
};
setEditData(prev => ({ ...prev, [field]: value }))
}
const handleApiAccessChange = (index: number, value: string) => {
const newApiAccess = [...(editData.api_access || [])];
newApiAccess[index] = value;
setEditData(prev => ({ ...prev, api_access: newApiAccess }));
};
setEditData(prev => ({
...prev,
api_access: prev.api_access?.map((api, i) => i === index ? value : api) || []
}))
}
const addApiAccess = () => {
setEditData(prev => ({
...prev,
api_access: [...(prev.api_access || []), '']
}));
};
if (newApi.trim() && !editData.api_access?.includes(newApi.trim())) {
setEditData(prev => ({
...prev,
api_access: [...(prev.api_access || []), newApi.trim()]
}))
setNewApi('')
}
}
const removeApiAccess = (index: number) => {
const newApiAccess = [...(editData.api_access || [])];
newApiAccess.splice(index, 1);
setEditData(prev => ({ ...prev, api_access: newApiAccess }));
};
setEditData(prev => ({
...prev,
api_access: prev.api_access?.filter((_, i) => i !== index) || []
}))
}
if (loading) {
return (
<>
<div className="groups-topbar">Doorman</div>
<div className="groups-root">
<aside className="groups-sidebar">
<div className="groups-sidebar-title">Menu</div>
<ul className="groups-sidebar-list">
{menuItems.map((item, idx) => (
item.href ? (
<li key={item.label} className={`groups-sidebar-item${idx === 4 ? ' active' : ''}`}>
<Link href={item.href} style={{ color: 'inherit', textDecoration: 'none', display: 'block', width: '100%' }}>{item.label}</Link>
</li>
) : (
<li key={item.label} className={`groups-sidebar-item${idx === 4 ? ' active' : ''}`}>{item.label}</li>
)
))}
</ul>
<button className="groups-logout-btn" onClick={handleLogout}>Logout</button>
</aside>
<main className="groups-main">
<div className="loading-spinner">
<div className="spinner"></div>
<p>Loading group...</p>
</div>
</main>
<Layout>
<div className="flex items-center justify-center py-12">
<div className="text-center">
<div className="spinner mx-auto mb-4"></div>
<p className="text-gray-600 dark:text-gray-400">Loading group details...</p>
</div>
</div>
</>
);
</Layout>
)
}
if (!group) {
if (error && !group) {
return (
<>
<div className="groups-topbar">Doorman</div>
<div className="groups-root">
<aside className="groups-sidebar">
<div className="groups-sidebar-title">Menu</div>
<ul className="groups-sidebar-list">
{menuItems.map((item, idx) => (
item.href ? (
<li key={item.label} className={`groups-sidebar-item${idx === 4 ? ' active' : ''}`}>
<Link href={item.href} style={{ color: 'inherit', textDecoration: 'none', display: 'block', width: '100%' }}>{item.label}</Link>
</li>
) : (
<li key={item.label} className={`groups-sidebar-item${idx === 4 ? ' active' : ''}`}>{item.label}</li>
)
))}
</ul>
<button className="groups-logout-btn" onClick={handleLogout}>Logout</button>
</aside>
<main className="groups-main">
<div className="error-container">
<div className="error-message">Group not found</div>
<Link href="/groups" className="back-link">Back to Groups</Link>
<Layout>
<div className="space-y-6">
<div className="page-header">
<div>
<h1 className="page-title">Group Details</h1>
</div>
</main>
<button onClick={handleBack} className="btn btn-secondary">
<svg className="h-4 w-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
Back to Groups
</button>
</div>
<div className="rounded-lg bg-error-50 border border-error-200 p-4 dark:bg-error-900/20 dark:border-error-800">
<div className="flex">
<svg className="h-5 w-5 text-error-400 dark:text-error-500 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div className="ml-3">
<p className="text-sm text-error-700 dark:text-error-300">{error}</p>
</div>
</div>
</div>
</div>
</>
);
</Layout>
)
}
return (
<>
<div className="groups-topbar">Doorman</div>
<div className="groups-root">
<aside className="groups-sidebar">
<div className="groups-sidebar-title">Menu</div>
<ul className="groups-sidebar-list">
{menuItems.map((item, idx) => (
item.href ? (
<li key={item.label} className={`groups-sidebar-item${idx === 4 ? ' active' : ''}`}>
<Link href={item.href} style={{ color: 'inherit', textDecoration: 'none', display: 'block', width: '100%' }}>{item.label}</Link>
</li>
) : (
<li key={item.label} className={`groups-sidebar-item${idx === 4 ? ' active' : ''}`}>{item.label}</li>
)
))}
</ul>
<button className="groups-logout-btn" onClick={handleLogout}>Logout</button>
</aside>
<main className="groups-main">
<div className="groups-header">
<button className="back-button" onClick={() => router.push('/groups')}>
<span className="back-arrow"></span>
Back to Groups
</button>
<h1 className="groups-title">Group Details</h1>
<Layout>
<div className="space-y-6">
{/* Page Header */}
<div className="page-header">
<div>
<h1 className="page-title">{group?.group_name || 'Group Details'}</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">
Manage group configuration and API access
</p>
</div>
<div className="flex gap-2">
{!isEditing ? (
<button className="edit-button" onClick={handleEdit}>
Edit Group
</button>
) : (
<div className="edit-actions">
<button className="save-button" onClick={handleSave} disabled={saving}>
{saving ? 'Saving...' : 'Save Changes'}
<>
<button onClick={handleEdit} className="btn btn-primary">
<svg className="h-4 w-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
Edit Group
</button>
<button className="cancel-button" onClick={handleCancel} disabled={saving}>
Cancel
</button>
<button className="delete-button" onClick={() => setShowDeleteModal(true)} disabled={saving}>
<button onClick={() => setShowDeleteModal(true)} className="btn btn-error">
<svg className="h-4 w-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
Delete Group
</button>
</div>
</>
) : (
<>
<button onClick={handleSave} disabled={saving} className="btn btn-primary">
{saving ? (
<div className="flex items-center">
<div className="spinner mr-2"></div>
Saving...
</div>
) : (
<>
<svg className="h-4 w-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
Save Changes
</>
)}
</button>
<button onClick={handleCancel} className="btn btn-secondary">
Cancel
</button>
</>
)}
<button onClick={handleBack} className="btn btn-ghost">
<svg className="h-4 w-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
Back
</button>
</div>
</div>
{error && (
<div className="error-container">
<div className="error-message">{error}</div>
{/* Success Message */}
{success && (
<div className="rounded-lg bg-success-50 border border-success-200 p-4 dark:bg-success-900/20 dark:border-success-800">
<div className="flex">
<svg className="h-5 w-5 text-success-400 dark:text-success-500 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div className="ml-3">
<p className="text-sm text-success-700 dark:text-success-300">{success}</p>
</div>
</div>
)}
</div>
)}
<div className="groups-detail-content">
<div className="groups-detail-card">
<div className="groups-detail-section">
<h2 className="section-title">Basic Information</h2>
<div className="info-grid">
<div className="info-item">
<label className="info-label">Group Name</label>
{isEditing ? (
<input
type="text"
className="edit-input"
value={editData.group_name || ''}
onChange={(e) => handleInputChange('group_name', e.target.value)}
disabled={saving}
/>
) : (
<span className="info-value">{group.group_name}</span>
)}
</div>
<div className="info-item">
<label className="info-label">Description</label>
{isEditing ? (
<textarea
className="edit-input"
value={editData.group_description || ''}
onChange={(e) => handleInputChange('group_description', e.target.value)}
disabled={saving}
rows={3}
/>
) : (
<span className="info-value">{group.group_description || 'No description'}</span>
)}
</div>
{/* Error Message */}
{error && (
<div className="rounded-lg bg-error-50 border border-error-200 p-4 dark:bg-error-900/20 dark:border-error-800">
<div className="flex">
<svg className="h-5 w-5 text-error-400 dark:text-error-500 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div className="ml-3">
<p className="text-sm text-error-700 dark:text-error-300">{error}</p>
</div>
</div>
</div>
)}
{group && (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Basic Information */}
<div className="card">
<div className="card-header">
<h3 className="card-title">Basic Information</h3>
</div>
<div className="p-6 space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Group Name
</label>
{isEditing ? (
<input
type="text"
value={editData.group_name || ''}
onChange={(e) => handleInputChange('group_name', e.target.value)}
className="input"
placeholder="Enter group name"
/>
) : (
<p className="text-gray-900 dark:text-white">{group.group_name}</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Description
</label>
{isEditing ? (
<textarea
value={editData.group_description || ''}
onChange={(e) => handleInputChange('group_description', e.target.value)}
className="input resize-none"
rows={3}
placeholder="Enter group description"
/>
) : (
<p className="text-gray-600 dark:text-gray-400">{group.group_description || 'No description'}</p>
)}
</div>
</div>
</div>
<div className="groups-detail-section">
<h2 className="section-title">API Access</h2>
{isEditing ? (
<div className="groups-edit">
{(editData.api_access || []).map((api, index) => (
<div key={index} className="group-edit-item">
<input
type="text"
className="edit-input"
value={api}
onChange={(e) => handleApiAccessChange(index, e.target.value)}
placeholder="Enter API name/version (e.g., customer/v1)"
disabled={saving}
/>
<button
type="button"
className="remove-button"
onClick={() => removeApiAccess(index)}
disabled={saving}
>
Remove
</button>
</div>
))}
<button
type="button"
className="add-button"
onClick={addApiAccess}
disabled={saving}
>
+ Add API Access
{/* API Access */}
<div className="card">
<div className="card-header">
<h3 className="card-title">API Access</h3>
</div>
<div className="p-6 space-y-4">
{isEditing && (
<div className="flex gap-2">
<input
type="text"
value={newApi}
onChange={(e) => setNewApi(e.target.value)}
className="input flex-1"
placeholder="Enter API name to grant access"
onKeyPress={(e) => e.key === 'Enter' && addApiAccess()}
/>
<button onClick={addApiAccess} className="btn btn-primary">
Add
</button>
</div>
) : (
<div className="groups-container">
{group.api_access && group.api_access.length > 0 ? (
<div className="groups-list">
{group.api_access.map((api, index) => (
<span key={index} className="group-tag">
{api}
</span>
))}
</div>
) : (
<span className="no-groups">No API access configured</span>
)}
</div>
)}
<div className="space-y-2">
{(isEditing ? editData.api_access : group.api_access)?.map((api, index) => (
<div key={index} className="flex items-center gap-2">
{isEditing ? (
<input
type="text"
value={api}
onChange={(e) => handleApiAccessChange(index, e.target.value)}
className="input flex-1"
placeholder="Enter API name"
/>
) : (
<span className="text-sm bg-blue-100 dark:bg-blue-900/20 text-blue-800 dark:text-blue-200 px-3 py-1 rounded-full flex-1">
{api}
</span>
)}
{isEditing && (
<button
onClick={() => removeApiAccess(index)}
className="text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-200"
>
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
)}
</div>
))}
</div>
{(!isEditing ? group.api_access : editData.api_access)?.length === 0 && (
<p className="text-gray-500 dark:text-gray-400 text-sm">No APIs assigned</p>
)}
</div>
</div>
</div>
</main>
</div>
)}
{/* Delete Confirmation Modal */}
{showDeleteModal && (
<div className="modal-overlay">
<div className="modal-content">
<div className="modal-header">
<h2 className="modal-title">Delete Group</h2>
<button className="modal-close" onClick={() => setShowDeleteModal(false)}>
×
</button>
</div>
<div className="modal-body">
<p className="modal-message">
This action cannot be undone. This will permanently delete the group <strong>{group?.group_name}</strong>.
{/* Delete Modal */}
{showDeleteModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center">
<div className="fixed inset-0 bg-black/50" onClick={() => setShowDeleteModal(false)}></div>
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-md w-full mx-4 relative z-10">
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-4">Delete Group</h3>
<p className="text-gray-600 dark:text-gray-400 mb-4">
This action cannot be undone. This will permanently delete the group "{group?.group_name}".
</p>
<p className="modal-warning">
To confirm deletion, please type the group name <strong>{group?.group_name}</strong> in the field below:
<p className="text-sm text-gray-500 dark:text-gray-400 mb-4">
Please type <strong>{group?.group_name}</strong> to confirm.
</p>
<input
type="text"
className="modal-input"
placeholder="Enter group name to confirm"
value={deleteConfirmation}
onChange={(e) => setDeleteConfirmation(e.target.value)}
autoFocus
className="input w-full mb-4"
placeholder="Enter group name to confirm"
/>
</div>
<div className="modal-footer">
<button
className="modal-cancel-button"
onClick={() => setShowDeleteModal(false)}
disabled={deleting}
>
Cancel
</button>
<button
className="modal-delete-button"
onClick={handleDelete}
disabled={deleting || deleteConfirmation !== group?.group_name}
>
{deleting ? 'Deleting...' : 'Delete Group'}
</button>
<div className="flex gap-2">
<button
onClick={handleDelete}
disabled={deleteConfirmation !== group?.group_name || deleting}
className="btn btn-error flex-1"
>
{deleting ? (
<div className="flex items-center justify-center">
<div className="spinner mr-2"></div>
Deleting...
</div>
) : (
'Delete Group'
)}
</button>
<button onClick={() => setShowDeleteModal(false)} className="btn btn-secondary flex-1">
Cancel
</button>
</div>
</div>
</div>
</div>
)}
</>
);
};
)}
</div>
</Layout>
)
}
export default GroupDetailPage;
export default GroupDetailPage
+157 -170
View File
@@ -1,87 +1,59 @@
'use client'
import React, { useState, useEffect } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import '../groups.css';
import React, { useState } from 'react'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
import Layout from '@/components/Layout'
interface CreateGroupData {
group_name: string;
group_description: string;
api_access: string[];
group_name: string
group_description: string
api_access: string[]
}
const menuItems = [
{ label: 'Dashboard', href: '/dashboard' },
{ label: 'APIs', href: '/apis' },
{ label: 'Routings', href: '/routings' },
{ label: 'Users', href: '/users' },
{ label: 'Groups', href: '/groups' },
{ label: 'Roles', href: '/roles' },
{ label: 'Monitor', href: '/monitor' },
{ label: 'Logs', href: '/logging' },
{ label: 'Security', href: '/security' },
{ label: 'Settings', href: '/settings' },
];
const handleLogout = () => {
localStorage.clear();
sessionStorage.clear();
setTimeout(() => {
window.location.replace('/');
}, 50);
};
const AddGroupPage = () => {
const router = useRouter();
const [theme, setTheme] = useState('light');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const router = useRouter()
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [formData, setFormData] = useState<CreateGroupData>({
group_name: '',
group_description: '',
api_access: []
});
const [newApi, setNewApi] = useState('');
useEffect(() => {
const savedTheme = localStorage.getItem('theme') || 'light';
setTheme(savedTheme);
document.documentElement.setAttribute('data-theme', savedTheme);
}, []);
})
const [newApi, setNewApi] = useState('')
const handleInputChange = (field: keyof CreateGroupData, value: any) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
setFormData(prev => ({ ...prev, [field]: value }))
}
const addApi = () => {
if (newApi.trim() && !formData.api_access.includes(newApi.trim())) {
setFormData(prev => ({
...prev,
api_access: [...prev.api_access, newApi.trim()]
}));
setNewApi('');
}))
setNewApi('')
}
};
}
const removeApi = (index: number) => {
setFormData(prev => ({
...prev,
api_access: prev.api_access.filter((_, i) => i !== index)
}));
};
}))
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
e.preventDefault()
if (!formData.group_name.trim()) {
setError('Group name is required');
return;
setError('Group name is required')
return
}
try {
setLoading(true);
setError(null);
setLoading(true)
setError(null)
const response = await fetch('http://localhost:3002/platform/group', {
method: 'POST',
@@ -92,149 +64,164 @@ const AddGroupPage = () => {
'Cookie': `access_token_cookie=${document.cookie.split('; ').find(row => row.startsWith('access_token_cookie='))?.split('=')[1]}`
},
body: JSON.stringify(formData)
});
})
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error_message || 'Failed to create group');
const errorData = await response.json()
throw new Error(errorData.detail || 'Failed to create group')
}
// Redirect back to groups list after successful creation
router.push('/groups');
router.push('/groups')
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to create group');
if (err instanceof Error) {
setError(err.message)
} else {
setError('Failed to create group. Please try again.')
}
} finally {
setLoading(false);
setLoading(false)
}
};
}
return (
<>
<div className="groups-topbar">
Doorman
</div>
<div className="groups-root">
<aside className="groups-sidebar">
<div className="groups-sidebar-title">Menu</div>
<ul className="groups-sidebar-list">
{menuItems.map((item, idx) => (
item.href ? (
<li key={item.label} className={`groups-sidebar-item${idx === 4 ? ' active' : ''}`}>
<Link href={item.href} style={{ color: 'inherit', textDecoration: 'none', display: 'block', width: '100%' }}>{item.label}</Link>
</li>
) : (
<li key={item.label} className={`groups-sidebar-item${idx === 4 ? ' active' : ''}`}>{item.label}</li>
)
))}
</ul>
<button className="groups-logout-btn" onClick={handleLogout}>
Logout
</button>
</aside>
<main className="groups-main">
<div className="groups-header-row">
<Link href="/groups" className="back-button" style={{ textDecoration: 'none', display: 'inline-block' }}>
<span className="back-arrow"></span>
Back to Groups
</Link>
<h1 className="groups-title">Add Group</h1>
<Layout>
<div className="space-y-6">
{/* Page Header */}
<div className="page-header">
<div>
<h1 className="page-title">Add Group</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">
Create a new user group with API access permissions
</p>
</div>
</div>
{error && (
<div className="error-container">
<div className="error-message">
{error}
{/* Error Message */}
{error && (
<div className="rounded-lg bg-error-50 border border-error-200 p-4 dark:bg-error-900/20 dark:border-error-800">
<div className="flex">
<svg className="h-5 w-5 text-error-400 dark:text-error-500 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div className="ml-3">
<p className="text-sm text-error-700 dark:text-error-300">{error}</p>
</div>
</div>
)}
</div>
)}
<div className="add-group-form">
<form onSubmit={handleSubmit}>
<div className="form-section">
<h2 className="section-title">Basic Information</h2>
<div className="form-group">
<label className="form-label">Group Name *</label>
{/* Form */}
<div className="card max-w-2xl">
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label htmlFor="group_name" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Group Name *
</label>
<input
type="text"
id="group_name"
name="group_name"
className="input"
placeholder="Enter group name"
value={formData.group_name}
onChange={(e) => handleInputChange('group_name', e.target.value)}
disabled={loading}
required
/>
</div>
<div>
<label htmlFor="group_description" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Description
</label>
<textarea
id="group_description"
name="group_description"
rows={4}
className="input resize-none"
placeholder="Describe the purpose of this group..."
value={formData.group_description}
onChange={(e) => handleInputChange('group_description', e.target.value)}
disabled={loading}
/>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
Optional description of the group's purpose
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
API Access
</label>
<div className="space-y-3">
<div className="flex gap-2">
<input
type="text"
className="form-input"
value={formData.group_name}
onChange={(e) => handleInputChange('group_name', e.target.value)}
placeholder="Enter group name"
maxLength={50}
required
className="input flex-1"
placeholder="Enter API name to grant access"
value={newApi}
onChange={(e) => setNewApi(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && (e.preventDefault(), addApi())}
disabled={loading}
/>
<button
type="button"
onClick={addApi}
disabled={loading || !newApi.trim()}
className="btn btn-primary"
>
Add
</button>
</div>
<div className="form-group">
<label className="form-label">Description</label>
<textarea
className="form-input"
value={formData.group_description}
onChange={(e) => handleInputChange('group_description', e.target.value)}
placeholder="Enter group description"
maxLength={255}
rows={3}
/>
</div>
</div>
<div className="form-section">
<h2 className="section-title">API Access</h2>
<div className="form-group">
<label className="form-label">APIs</label>
<div className="api-access-container">
<div className="api-access-list">
{formData.api_access.map((api, index) => (
<span key={index} className="api-access-tag">
{api}
<button
type="button"
className="remove-api-btn"
onClick={() => removeApi(index)}
>
×
</button>
</span>
))}
</div>
<div className="add-api">
<input
type="text"
className="form-input"
placeholder="Enter API name"
value={newApi}
onChange={(e) => setNewApi(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && (e.preventDefault(), addApi())}
/>
<div className="flex flex-wrap gap-2">
{formData.api_access.map((api, index) => (
<div key={index} className="flex items-center gap-2 bg-blue-100 dark:bg-blue-900/20 text-blue-800 dark:text-blue-200 px-3 py-1 rounded-full">
<span className="text-sm">{api}</span>
<button
type="button"
className="add-button"
onClick={addApi}
onClick={() => removeApi(index)}
className="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-200"
disabled={loading}
>
Add API
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
))}
</div>
{formData.api_access.length === 0 && (
<p className="text-gray-500 dark:text-gray-400 text-sm">No APIs assigned yet</p>
)}
</div>
</div>
<div className="form-actions">
<Link href="/groups" className="cancel-button" style={{ textDecoration: 'none', display: 'inline-block' }}>
Cancel
</Link>
<button
type="submit"
className="save-button"
disabled={loading}
>
{loading ? 'Creating...' : 'Create Group'}
</button>
</div>
</form>
</div>
</main>
<div className="flex gap-4 pt-6 border-t border-gray-200 dark:border-gray-700">
<button
type="submit"
disabled={loading}
className="btn btn-primary flex-1"
>
{loading ? (
<div className="flex items-center justify-center">
<div className="spinner mr-2"></div>
Creating Group...
</div>
) : (
'Create Group'
)}
</button>
<Link href="/groups" className="btn btn-secondary flex-1">
Cancel
</Link>
</div>
</form>
</div>
</div>
</>
);
};
</Layout>
)
}
export default AddGroupPage;
export default AddGroupPage
+164 -146
View File
@@ -1,61 +1,33 @@
'use client'
import React, { useState, useEffect } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import './groups.css';
import React, { useState, useEffect } from 'react'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
import Layout from '@/components/Layout'
interface Group {
group_name: string;
group_description: string;
api_access?: string[];
group_name: string
group_description: string
api_access?: string[]
}
const menuItems = [
{ label: 'Dashboard', href: '/dashboard' },
{ label: 'APIs', href: '/apis' },
{ label: 'Routings', href: '/routings' },
{ label: 'Users', href: '/users' },
{ label: 'Groups', href: '/groups' },
{ label: 'Roles', href: '/roles' },
{ label: 'Monitor', href: '/monitor' },
{ label: 'Logs', href: '/logging' },
{ label: 'Security', href: '/security' },
{ label: 'Settings', href: '/settings' },
];
const handleLogout = () => {
localStorage.clear();
sessionStorage.clear();
setTimeout(() => {
window.location.replace('/');
}, 50);
};
const GroupsPage = () => {
const router = useRouter();
const [theme, setTheme] = useState('light');
const [groups, setGroups] = useState<Group[]>([]);
const [allGroups, setAllGroups] = useState<Group[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [searchTerm, setSearchTerm] = useState('');
const [sortBy, setSortBy] = useState('group_name');
const router = useRouter()
const [groups, setGroups] = useState<Group[]>([])
const [allGroups, setAllGroups] = useState<Group[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [searchTerm, setSearchTerm] = useState('')
const [sortBy, setSortBy] = useState('group_name')
useEffect(() => {
const savedTheme = localStorage.getItem('theme') || 'light';
setTheme(savedTheme);
document.documentElement.setAttribute('data-theme', savedTheme);
}, []);
useEffect(() => {
fetchGroups();
}, []);
fetchGroups()
}, [])
const fetchGroups = async () => {
try {
setLoading(true);
setError(null);
setLoading(true)
setError(null)
const response = await fetch(`http://localhost:3002/platform/group/all?page=1&page_size=10`, {
credentials: 'include',
headers: {
@@ -63,167 +35,213 @@ const GroupsPage = () => {
'Content-Type': 'application/json',
'Cookie': `access_token_cookie=${document.cookie.split('; ').find(row => row.startsWith('access_token_cookie='))?.split('=')[1]}`
}
});
})
if (!response.ok) {
throw new Error('Failed to load groups');
throw new Error('Failed to load groups')
}
const data = await response.json();
setAllGroups(data.groups);
setGroups(data.groups);
const data = await response.json()
setAllGroups(data.groups)
setGroups(data.groups)
} catch (err) {
setError('Failed to load groups. Please try again later.');
setGroups([]);
setAllGroups([]);
setError('Failed to load groups. Please try again later.')
setGroups([])
setAllGroups([])
} finally {
setLoading(false);
setLoading(false)
}
};
}
const handleSearch = (e: React.FormEvent) => {
e.preventDefault();
e.preventDefault()
if (!searchTerm.trim()) {
setGroups(allGroups);
return;
setGroups(allGroups)
return
}
const filteredGroups = allGroups.filter(group =>
group.group_name.toLowerCase().includes(searchTerm.toLowerCase()) ||
group.group_description?.toLowerCase().includes(searchTerm.toLowerCase()) ||
group.api_access?.some(api => api.toLowerCase().includes(searchTerm.toLowerCase()))
);
setGroups(filteredGroups);
};
)
setGroups(filteredGroups)
}
const handleSort = (sortField: string) => {
setSortBy(sortField);
setSortBy(sortField)
const sortedGroups = [...groups].sort((a, b) => {
if (sortField === 'group_name') {
return a.group_name.localeCompare(b.group_name);
return a.group_name.localeCompare(b.group_name)
} else if (sortField === 'api_access') {
return (a.api_access?.length || 0) - (b.api_access?.length || 0);
return (a.api_access?.length || 0) - (b.api_access?.length || 0)
}
return 0;
});
setGroups(sortedGroups);
};
return 0
})
setGroups(sortedGroups)
}
const handleGroupClick = (group: Group) => {
// Store group data in sessionStorage for the detail page
sessionStorage.setItem('selectedGroup', JSON.stringify(group));
router.push(`/groups/${group.group_name}`);
};
sessionStorage.setItem('selectedGroup', JSON.stringify(group))
router.push(`/groups/${group.group_name}`)
}
return (
<>
<div className="groups-topbar">
Doorman
</div>
<div className="groups-root">
<aside className="groups-sidebar">
<div className="groups-sidebar-title">Menu</div>
<ul className="groups-sidebar-list">
{menuItems.map((item, idx) => (
item.href ? (
<li key={item.label} className={`groups-sidebar-item${idx === 4 ? ' active' : ''}`}>
<Link href={item.href} style={{ color: 'inherit', textDecoration: 'none', display: 'block', width: '100%' }}>{item.label}</Link>
</li>
) : (
<li key={item.label} className={`groups-sidebar-item${idx === 4 ? ' active' : ''}`}>{item.label}</li>
)
))}
</ul>
<button className="groups-logout-btn" onClick={handleLogout}>
Logout
</button>
</aside>
<main className="groups-main">
<div className="groups-header-row">
<h1 className="groups-title">Groups</h1>
<button className="refresh-button" onClick={fetchGroups}>
<span className="refresh-icon"></span>
Refresh
</button>
<Layout>
<div className="space-y-6">
{/* Page Header */}
<div className="page-header">
<div>
<h1 className="page-title">Groups</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">
Manage user groups and API access permissions
</p>
</div>
<Link href="/groups/add" className="btn btn-primary">
<svg className="h-4 w-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
Add Group
</Link>
</div>
<div className="groups-controls">
<form className="groups-search-box" onSubmit={handleSearch}>
<input
type="text"
className="groups-search-input"
placeholder="Search groups..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
<button type="submit" className="groups-search-btn">Search</button>
<Link href="/groups/add" className="groups-add-btn" style={{ textDecoration: 'none', display: 'inline-block' }}>
Add Group
</Link>
{/* Search and Filters */}
<div className="card">
<div className="flex flex-col sm:flex-row gap-4">
<form onSubmit={handleSearch} className="flex-1">
<div className="relative">
<svg className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
<input
type="text"
className="search-input"
placeholder="Search groups by name, description, or API access..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
</form>
<div className="groups-sort-group">
<button
className={`groups-sort-btn ${sortBy === 'group_name' ? 'active' : ''}`}
<div className="flex gap-2">
<button
onClick={() => handleSort('group_name')}
className={`btn ${sortBy === 'group_name' ? 'btn-primary' : 'btn-secondary'}`}
>
Name
</button>
<button
className={`groups-sort-btn ${sortBy === 'api_access' ? 'active' : ''}`}
<button
onClick={() => handleSort('api_access')}
className={`btn ${sortBy === 'api_access' ? 'btn-primary' : 'btn-secondary'}`}
>
API Access
</button>
</div>
</div>
</div>
{error && (
<div className="error-container">
<div className="error-message">
{error}
{/* Error Message */}
{error && (
<div className="rounded-lg bg-error-50 border border-error-200 p-4 dark:bg-error-900/20 dark:border-error-800">
<div className="flex">
<svg className="h-5 w-5 text-error-400 dark:text-error-500 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div className="ml-3">
<p className="text-sm text-error-700 dark:text-error-300">{error}</p>
</div>
</div>
)}
</div>
)}
{loading ? (
<div className="loading-spinner">
<div className="spinner"></div>
<p>Loading groups...</p>
{/* Loading State */}
{loading ? (
<div className="card">
<div className="flex items-center justify-center py-12">
<div className="text-center">
<div className="spinner mx-auto mb-4"></div>
<p className="text-gray-600 dark:text-gray-400">Loading groups...</p>
</div>
</div>
) : (
<div className="groups-table-panel">
<table className="groups-table">
</div>
) : (
/* Groups Table */
<div className="card">
<div className="overflow-x-auto">
<table className="table">
<thead>
<tr>
<th>Name</th>
<th>Description</th>
<th>API Access</th>
<th></th>
</tr>
</thead>
<tbody>
{groups.map((group) => (
<tr
key={group.group_name}
key={group.group_name}
onClick={() => handleGroupClick(group)}
style={{ cursor: 'pointer' }}
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#f5f5f5'}
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = ''}
className="cursor-pointer hover:bg-gray-50 dark:hover:bg-dark-surfaceHover transition-colors"
>
<td><b>{group.group_name}</b></td>
<td>{group.group_description || 'No description'}</td>
<td>
<span className="groups-api-badge">
<div className="flex items-center">
<div className="h-8 w-8 rounded-lg bg-gradient-to-br from-green-400 to-green-600 flex items-center justify-center mr-3">
<svg className="h-4 w-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
</svg>
</div>
<div>
<p className="font-medium text-gray-900 dark:text-white">{group.group_name}</p>
</div>
</div>
</td>
<td>
<p className="text-sm text-gray-600 dark:text-gray-400 max-w-xs truncate">
{group.group_description || 'No description'}
</p>
</td>
<td>
<span className="badge badge-success">
{group.api_access?.length || 0} API{(group.api_access?.length || 0) !== 1 ? 's' : ''}
</span>
</td>
<td>
<button className="btn btn-ghost btn-sm">
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</main>
</div>
</>
);
};
export default GroupsPage;
{/* Empty State */}
{groups.length === 0 && !loading && (
<div className="text-center py-12">
<div className="h-16 w-16 mx-auto mb-4 rounded-full bg-gray-100 dark:bg-gray-800 flex items-center justify-center">
<svg className="h-8 w-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
</svg>
</div>
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">No groups found</h3>
<p className="text-gray-600 dark:text-gray-400 mb-4">
{searchTerm ? 'Try adjusting your search terms.' : 'Get started by creating your first user group.'}
</p>
<Link href="/groups/add" className="btn btn-primary">
<svg className="h-4 w-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
Add Group
</Link>
</div>
)}
</div>
)}
</div>
</Layout>
)
}
export default GroupsPage
+20 -2
View File
@@ -1,11 +1,29 @@
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import './globals.css'
const inter = Inter({ subsets: ['latin'] })
export const metadata: Metadata = {
title: 'Doorman - API Gateway Management',
description: 'Modern API gateway management platform',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>{children}</body>
<html lang="en" className="h-full">
<head>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet" />
</head>
<body className={`${inter.className} h-full bg-gray-50 dark:bg-dark-bg text-gray-900 dark:text-dark-text antialiased transition-colors duration-200`}>
{children}
</body>
</html>
)
}
+342 -331
View File
@@ -1,77 +1,51 @@
'use client'
import React, { useState, useEffect } from 'react';
import { format } from 'date-fns';
import { ChangeEvent } from 'react';
import './logging.css';
const menuItems = [
{ label: 'Dashboard', href: '/dashboard' },
{ label: 'APIs', href: '/apis' },
{ label: 'Routings', href: '/routings' },
{ label: 'Users', href: '/users' },
{ label: 'Groups', href: '/groups' },
{ label: 'Roles', href: '/roles' },
{ label: 'Monitor', href: '/monitor' },
{ label: 'Logs', href: '/logging' },
{ label: 'Security', href: '/security' },
{ label: 'Settings', href: '/settings' },
];
const handleLogout = () => {
localStorage.clear();
sessionStorage.clear();
setTimeout(() => {
window.location.replace('/');
}, 50);
};
import React, { useState, useEffect } from 'react'
import { format } from 'date-fns'
import { ChangeEvent } from 'react'
import Layout from '@/components/Layout'
interface Log {
timestamp: string;
request_id?: string;
level: string;
message: string;
source: string;
user?: string;
endpoint?: string;
method?: string;
ipAddress?: string;
responseTime?: number;
timestamp: string
request_id?: string
level: string
message: string
source: string
user?: string
endpoint?: string
method?: string
ipAddress?: string
responseTime?: number
}
interface FilterState {
startDate: string;
endDate: string;
startTime: string;
endTime: string;
user: string;
endpoint: string;
request_id: string;
method: string;
ipAddress: string;
minResponseTime: string;
maxResponseTime: string;
level: string;
startDate: string
endDate: string
startTime: string
endTime: string
user: string
endpoint: string
request_id: string
method: string
ipAddress: string
minResponseTime: string
maxResponseTime: string
level: string
}
export default function LogsPage() {
const [logs, setLogs] = useState<Log[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [showMoreFilters, setShowMoreFilters] = useState(false);
const [exporting, setExporting] = useState(false);
const [logs, setLogs] = useState<Log[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [showMoreFilters, setShowMoreFilters] = useState(false)
const [exporting, setExporting] = useState(false)
const [filters, setFilters] = useState<FilterState>(() => {
// Set default to last 30 minutes of today
const now = new Date();
const today = now.toISOString().split('T')[0]; // Always use today's date
const now = new Date()
const today = now.toISOString().split('T')[0]
// Calculate 30 minutes ago, but ensure we stay within today
const thirtyMinutesAgo = new Date(now.getTime() - 30 * 60 * 1000);
const startTime = thirtyMinutesAgo.toTimeString().slice(0, 5);
const endTime = now.toTimeString().slice(0, 5);
const thirtyMinutesAgo = new Date(now.getTime() - 30 * 60 * 1000)
const startTime = thirtyMinutesAgo.toTimeString().slice(0, 5)
const endTime = now.toTimeString().slice(0, 5)
return {
startDate: today,
@@ -86,87 +60,57 @@ export default function LogsPage() {
minResponseTime: '',
maxResponseTime: '',
level: ''
};
});
}
})
useEffect(() => {
fetchLogs();
}, [filters]);
fetchLogs()
}, [filters])
const fetchLogs = async () => {
try {
setLoading(true);
setError(null);
setLoading(true)
setError(null)
const queryParams = new URLSearchParams();
if (filters.startDate) queryParams.append('start_date', filters.startDate);
if (filters.endDate) queryParams.append('end_date', filters.endDate);
if (filters.startTime) queryParams.append('start_time', filters.startTime);
if (filters.endTime) queryParams.append('end_time', filters.endTime);
if (filters.user) queryParams.append('user', filters.user);
if (filters.endpoint) queryParams.append('endpoint', filters.endpoint);
if (filters.request_id) queryParams.append('request_id', filters.request_id);
if (filters.method) queryParams.append('method', filters.method);
if (filters.ipAddress) queryParams.append('ip_address', filters.ipAddress);
if (filters.minResponseTime) queryParams.append('min_response_time', filters.minResponseTime);
if (filters.maxResponseTime) queryParams.append('max_response_time', filters.maxResponseTime);
if (filters.level) queryParams.append('level', filters.level);
const serverUrl = process.env.NEXT_PUBLIC_SERVER_URL || 'http://localhost:3002';
const url = `${serverUrl}/platform/logging/logs?${queryParams.toString()}`;
console.log('Fetching logs with URL:', url);
console.log('Filters:', filters);
const queryParams = new URLSearchParams()
Object.entries(filters).forEach(([key, value]) => {
if (value) queryParams.append(key, value)
})
const response = await fetch(`http://localhost:3002/platform/logging?${queryParams}`, {
credentials: 'include',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Cookie': `access_token_cookie=${document.cookie.split('; ').find(row => row.startsWith('access_token_cookie='))?.split('=')[1]}`
}
})
const response = await fetch(url, {
credentials: 'include'
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
const errorMessage = errorData.error_message || `HTTP ${response.status}: ${response.statusText}`;
throw new Error(errorMessage);
throw new Error('Failed to fetch logs')
}
const responseData = await response.json();
// Handle the response format
if (responseData && responseData.logs) {
setLogs(responseData.logs);
} else if (responseData && Array.isArray(responseData)) {
// Fallback for array format
setLogs(responseData);
} else {
// No logs found or unexpected format
setLogs([]);
console.warn('Unexpected response format:', responseData);
}
const data = await response.json()
setLogs(data.logs || [])
} catch (err) {
if (err instanceof Error) {
setError(err.message);
} else {
setError('An unknown error occurred');
}
setError('Failed to fetch logs. Please try again later.')
setLogs([])
} finally {
setLoading(false);
setLoading(false)
}
};
}
const handleFilterChange = (e: ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
const { name, value } = e.target;
setFilters(prev => ({
...prev,
[name]: value
}));
};
const { name, value } = e.target
setFilters(prev => ({ ...prev, [name]: value }))
}
const clearFilters = () => {
const now = new Date();
const today = now.toISOString().split('T')[0]; // Always use today's date
// Calculate 30 minutes ago, but ensure we stay within today
const thirtyMinutesAgo = new Date(now.getTime() - 30 * 60 * 1000);
const startTime = thirtyMinutesAgo.toTimeString().slice(0, 5);
const endTime = now.toTimeString().slice(0, 5);
const now = new Date()
const today = now.toISOString().split('T')[0]
const thirtyMinutesAgo = new Date(now.getTime() - 30 * 60 * 1000)
const startTime = thirtyMinutesAgo.toTimeString().slice(0, 5)
const endTime = now.toTimeString().slice(0, 5)
setFilters({
startDate: today,
@@ -181,200 +125,215 @@ export default function LogsPage() {
minResponseTime: '',
maxResponseTime: '',
level: ''
});
};
})
}
const exportLogs = async (format: 'json' | 'csv') => {
try {
setExporting(true);
const queryParams = new URLSearchParams();
queryParams.append('format', format);
if (filters.startDate) queryParams.append('start_date', filters.startDate);
if (filters.endDate) queryParams.append('end_date', filters.endDate);
if (filters.user) queryParams.append('user', filters.user);
if (filters.endpoint) queryParams.append('endpoint', filters.endpoint);
if (filters.level) queryParams.append('level', filters.level);
const serverUrl = process.env.NEXT_PUBLIC_SERVER_URL || 'http://localhost:3002';
const response = await fetch(`${serverUrl}/platform/logging/logs/download?${queryParams.toString()}`, {
credentials: 'include'
});
setExporting(true)
const queryParams = new URLSearchParams()
Object.entries(filters).forEach(([key, value]) => {
if (value) queryParams.append(key, value)
})
queryParams.append('format', format)
const response = await fetch(`http://localhost:3002/platform/logging/export?${queryParams}`, {
credentials: 'include',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Cookie': `access_token_cookie=${document.cookie.split('; ').find(row => row.startsWith('access_token_cookie='))?.split('=')[1]}`
}
})
if (!response.ok) {
throw new Error('Failed to export logs');
throw new Error('Failed to export logs')
}
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `logs_export_${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.${format}`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
const blob = await response.blob()
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `logs-${new Date().toISOString().split('T')[0]}.${format}`
document.body.appendChild(a)
a.click()
window.URL.revokeObjectURL(url)
document.body.removeChild(a)
} catch (err) {
if (err instanceof Error) {
setError(err.message);
} else {
setError('An unknown error occurred during export');
}
setError('Failed to export logs. Please try again later.')
} finally {
setExporting(false);
setExporting(false)
}
};
}
const getLevelColor = (level: string) => {
switch (level.toLowerCase()) {
case 'error': return 'text-red-600 dark:text-red-400'
case 'warn': return 'text-yellow-600 dark:text-yellow-400'
case 'info': return 'text-blue-600 dark:text-blue-400'
case 'debug': return 'text-gray-600 dark:text-gray-400'
default: return 'text-gray-600 dark:text-gray-400'
}
}
const getLevelBgColor = (level: string) => {
switch (level.toLowerCase()) {
case 'error': return 'bg-red-100 dark:bg-red-900/20 text-red-800 dark:text-red-200'
case 'warn': return 'bg-yellow-100 dark:bg-yellow-900/20 text-yellow-800 dark:text-yellow-200'
case 'info': return 'bg-blue-100 dark:bg-blue-900/20 text-blue-800 dark:text-blue-200'
case 'debug': return 'bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200'
default: return 'bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200'
}
}
return (
<>
<div className="logs-topbar">
Doorman
</div>
<div className="logs-root">
<aside className="logs-sidebar">
<div className="logs-sidebar-title">Menu</div>
<ul className="logs-sidebar-list">
{menuItems.map((item, idx) => (
item.href ? (
<li key={item.label} className={`logs-sidebar-item${idx === 7 ? ' active' : ''}`}>
<a href={item.href} style={{ color: 'inherit', textDecoration: 'none', display: 'block', width: '100%' }}>{item.label}</a>
</li>
) : (
<li key={item.label} className={`logs-sidebar-item${idx === 7 ? ' active' : ''}`}>{item.label}</li>
)
))}
</ul>
<button className="logs-logout-btn" onClick={handleLogout}>
Logout
</button>
</aside>
<main className="logs-main">
<div className="logs-header">
<h1>Logs</h1>
<div className="logs-header-controls">
<div className="export-buttons">
<button
className="export-button"
onClick={() => exportLogs('json')}
disabled={exporting}
>
{exporting ? 'Exporting...' : 'Export JSON'}
</button>
<button
className="export-button"
onClick={() => exportLogs('csv')}
disabled={exporting}
>
{exporting ? 'Exporting...' : 'Export CSV'}
</button>
</div>
<button className="refresh-button" onClick={fetchLogs}>
<span className="refresh-icon"></span>
Refresh
</button>
</div>
<Layout>
<div className="space-y-6">
{/* Page Header */}
<div className="page-header">
<div>
<h1 className="page-title">Logs</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">
View and analyze system logs and API requests
</p>
</div>
<div className="flex gap-2">
<button
onClick={() => exportLogs('json')}
disabled={exporting}
className="btn btn-secondary"
>
<svg className="h-4 w-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
Export JSON
</button>
<button
onClick={() => exportLogs('csv')}
disabled={exporting}
className="btn btn-secondary"
>
<svg className="h-4 w-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
Export CSV
</button>
</div>
</div>
<div className="filters-container">
<div className="filters-grid">
<div className="filter-group">
<label htmlFor="startDate">Start Date</label>
{/* Filters */}
<div className="card">
<div className="card-header">
<h3 className="card-title">Filters</h3>
<button
onClick={() => setShowMoreFilters(!showMoreFilters)}
className="btn btn-ghost btn-sm"
>
{showMoreFilters ? 'Show Less' : 'Show More'}
</button>
</div>
<div className="p-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Start Date
</label>
<input
type="date"
id="startDate"
name="startDate"
value={filters.startDate}
onChange={handleFilterChange}
className="input"
/>
</div>
<div className="filter-group">
<label htmlFor="endDate">End Date</label>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
End Date
</label>
<input
type="date"
id="endDate"
name="endDate"
value={filters.endDate}
onChange={handleFilterChange}
className="input"
/>
</div>
<div className="filter-group">
<label htmlFor="startTime">Start Time</label>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Start Time
</label>
<input
type="time"
id="startTime"
name="startTime"
value={filters.startTime}
onChange={handleFilterChange}
className="input"
/>
</div>
<div className="filter-group">
<label htmlFor="endTime">End Time</label>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
End Time
</label>
<input
type="time"
id="endTime"
name="endTime"
value={filters.endTime}
onChange={handleFilterChange}
className="input"
/>
</div>
</div>
<div className="filter-buttons">
<button
className="more-filters-button"
onClick={() => setShowMoreFilters(!showMoreFilters)}
>
{showMoreFilters ? 'Show Less Filters' : 'More Filters'}
<span className={`more-filters-icon ${showMoreFilters ? 'expanded' : ''}`}></span>
</button>
<button
className="clear-filters-button"
onClick={clearFilters}
>
Clear Filters
</button>
</div>
{showMoreFilters && (
<div className="more-filters-section">
<div className="filter-group">
<label htmlFor="user">User</label>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mt-4 pt-4 border-t border-gray-200 dark:border-gray-700">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
User
</label>
<input
type="text"
id="user"
name="user"
placeholder="Filter by user"
value={filters.user}
onChange={handleFilterChange}
placeholder="Filter by user"
className="input"
/>
</div>
<div className="filter-group">
<label htmlFor="endpoint">Endpoint</label>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Endpoint
</label>
<input
type="text"
id="endpoint"
name="endpoint"
placeholder="Filter by endpoint"
value={filters.endpoint}
onChange={handleFilterChange}
placeholder="Filter by endpoint"
className="input"
/>
</div>
<div className="filter-group">
<label htmlFor="request_id">Request ID</label>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Request ID
</label>
<input
type="text"
id="request_id"
name="request_id"
placeholder="Filter by request ID"
value={filters.request_id}
onChange={handleFilterChange}
placeholder="Filter by request ID"
className="input"
/>
</div>
<div className="filter-group">
<label htmlFor="method">HTTP Method</label>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Method
</label>
<select
id="method"
name="method"
value={filters.method}
onChange={handleFilterChange}
className="input"
>
<option value="">All Methods</option>
<option value="GET">GET</option>
@@ -382,127 +341,179 @@ export default function LogsPage() {
<option value="PUT">PUT</option>
<option value="DELETE">DELETE</option>
<option value="PATCH">PATCH</option>
<option value="HEAD">HEAD</option>
<option value="OPTIONS">OPTIONS</option>
</select>
</div>
<div className="filter-group">
<label htmlFor="ipAddress">IP Address</label>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
IP Address
</label>
<input
type="text"
id="ipAddress"
name="ipAddress"
placeholder="Filter by IP"
value={filters.ipAddress}
onChange={handleFilterChange}
placeholder="Filter by IP"
className="input"
/>
</div>
<div className="filter-group">
<label htmlFor="minResponseTime">Min Response Time (ms)</label>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Min Response Time (ms)
</label>
<input
type="number"
id="minResponseTime"
name="minResponseTime"
placeholder="Min time"
value={filters.minResponseTime}
onChange={handleFilterChange}
placeholder="Min time"
className="input"
/>
</div>
<div className="filter-group">
<label htmlFor="maxResponseTime">Max Response Time (ms)</label>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Max Response Time (ms)
</label>
<input
type="number"
id="maxResponseTime"
name="maxResponseTime"
placeholder="Max time"
value={filters.maxResponseTime}
onChange={handleFilterChange}
placeholder="Max time"
className="input"
/>
</div>
<div className="filter-group">
<label htmlFor="level">Log Level</label>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Log Level
</label>
<select
id="level"
name="level"
value={filters.level}
onChange={handleFilterChange}
className="input"
>
<option value="">All Levels</option>
<option value="ERROR">Error</option>
<option value="WARNING">Warning</option>
<option value="WARN">Warning</option>
<option value="INFO">Info</option>
<option value="DEBUG">Debug</option>
</select>
</div>
</div>
)}
<div className="flex gap-2 mt-6">
<button onClick={fetchLogs} className="btn btn-primary">
Apply Filters
</button>
<button onClick={clearFilters} className="btn btn-secondary">
Clear Filters
</button>
</div>
</div>
</div>
{error && (
<div className="error-message">
{error}
{/* Error Message */}
{error && (
<div className="rounded-lg bg-error-50 border border-error-200 p-4 dark:bg-error-900/20 dark:border-error-800">
<div className="flex">
<svg className="h-5 w-5 text-error-400 dark:text-error-500 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div className="ml-3">
<p className="text-sm text-error-700 dark:text-error-300">{error}</p>
</div>
</div>
)}
</div>
)}
{loading ? (
<div className="loading-spinner">
<div className="spinner"></div>
<p>Loading logs...</p>
{/* Loading State */}
{loading ? (
<div className="card">
<div className="flex items-center justify-center py-12">
<div className="text-center">
<div className="spinner mx-auto mb-4"></div>
<p className="text-gray-600 dark:text-gray-400">Loading logs...</p>
</div>
</div>
) : (
<div className="logs-container">
{logs.length === 0 ? (
<div className="no-logs">
No logs found for the selected time period
</div>
) : (
<table className="logs-table">
<thead>
<tr>
<th>Timestamp</th>
<th>Request ID</th>
<th>Level</th>
<th>Message</th>
<th>Source</th>
<th>User</th>
<th>Endpoint</th>
<th>Method</th>
<th>Response Time</th>
<th>IP Address</th>
</div>
) : (
/* Logs Table */
<div className="card">
<div className="overflow-x-auto">
<table className="table">
<thead>
<tr>
<th>Timestamp</th>
<th>Level</th>
<th>Message</th>
<th>User</th>
<th>Endpoint</th>
<th>Method</th>
<th>Response Time</th>
</tr>
</thead>
<tbody>
{logs.map((log, index) => (
<tr key={index} className="hover:bg-gray-50 dark:hover:bg-dark-surfaceHover transition-colors">
<td>
<p className="text-sm text-gray-900 dark:text-white">
{format(new Date(log.timestamp), 'MMM dd, yyyy HH:mm:ss')}
</p>
</td>
<td>
<span className={`badge ${getLevelBgColor(log.level)}`}>
{log.level}
</span>
</td>
<td>
<p className="text-sm text-gray-600 dark:text-gray-400 max-w-xs truncate">
{log.message}
</p>
</td>
<td>
<p className="text-sm text-gray-900 dark:text-white">
{log.user || '-'}
</p>
</td>
<td>
<p className="text-sm text-gray-600 dark:text-gray-400 max-w-xs truncate">
{log.endpoint || '-'}
</p>
</td>
<td>
<span className={`badge ${log.method === 'GET' ? 'badge-success' : log.method === 'POST' ? 'badge-primary' : 'badge-warning'}`}>
{log.method || '-'}
</span>
</td>
<td>
<p className="text-sm text-gray-900 dark:text-white">
{log.responseTime ? `${log.responseTime}ms` : '-'}
</p>
</td>
</tr>
</thead>
<tbody>
{logs.map((log, index) => (
<tr key={index} className={`log-row ${log.level.toLowerCase()}`}>
<td>{format(new Date(log.timestamp), 'yyyy-MM-dd HH:mm:ss')}</td>
<td>{log.request_id || '-'}</td>
<td>
<span className={`log-level ${log.level.toLowerCase()}`}>
{log.level}
</span>
</td>
<td>{log.message}</td>
<td>{log.source}</td>
<td>{log.user || '-'}</td>
<td>{log.endpoint || '-'}</td>
<td>{log.method || '-'}</td>
<td>
<span className={`status-code ${log.responseTime ? (log.responseTime < 100 ? 'success' : log.responseTime < 500 ? 'warning' : 'error') : ''}`}>
{log.responseTime ? `${log.responseTime}ms` : '-'}
</span>
</td>
<td>{log.ipAddress || '-'}</td>
</tr>
))}
</tbody>
</table>
)}
))}
</tbody>
</table>
</div>
)}
</main>
{/* Empty State */}
{logs.length === 0 && !loading && (
<div className="text-center py-12">
<div className="h-16 w-16 mx-auto mb-4 rounded-full bg-gray-100 dark:bg-gray-800 flex items-center justify-center">
<svg className="h-8 w-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</div>
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">No logs found</h3>
<p className="text-gray-600 dark:text-gray-400">
Try adjusting your filters or check back later for new logs.
</p>
</div>
)}
</div>
)}
</div>
</>
);
</Layout>
)
}
+156 -70
View File
@@ -1,24 +1,26 @@
'use client'
import React, { useState, useEffect } from 'react';
import "./login.css";
import React, { useState, useEffect } from 'react'
import { useRouter } from 'next/navigation'
const LoginPage = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [errorMessage, setErrorMessage] = useState('');
const [isClient, setIsClient] = useState(false);
const [theme, setTheme] = useState('light');
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [errorMessage, setErrorMessage] = useState('')
const [isLoading, setIsLoading] = useState(false)
const [theme, setTheme] = useState('light')
const router = useRouter()
useEffect(() => {
setIsClient(true);
const savedTheme = localStorage.getItem('theme') || 'light';
setTheme(savedTheme);
document.documentElement.setAttribute('data-theme', savedTheme);
}, []);
const savedTheme = localStorage.getItem('theme') || 'light'
setTheme(savedTheme)
document.documentElement.classList.toggle('dark', savedTheme === 'dark')
}, [])
const handleLogin = async (e: React.MouseEvent<HTMLButtonElement>): Promise<void> => {
e.preventDefault();
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault()
setIsLoading(true)
setErrorMessage('')
try {
const authResponse = await fetch(`http://localhost:3002/platform/authorization`, {
@@ -29,80 +31,164 @@ const LoginPage = () => {
'Accept': 'application/json'
},
body: JSON.stringify({ email, password }),
});
})
if (authResponse.ok) {
const data = await authResponse.json();
const data = await authResponse.json()
if (data.access_token) {
// Set the cookie with the access token
document.cookie = `access_token_cookie=${data.access_token}; path=/; domain=localhost`;
console.log('Access token cookie set:', document.cookie);
document.cookie = `access_token_cookie=${data.access_token}; path=/; domain=localhost`
router.push('/dashboard')
} else {
console.error('No access token in response:', data);
setErrorMessage('Login successful but no access token received');
return;
setErrorMessage('Login successful but no access token received')
}
window.location.href = '/dashboard';
} else {
const errorData = await authResponse.json();
setErrorMessage(errorData.detail || 'An error occurred');
const errorData = await authResponse.json()
setErrorMessage(errorData.detail || 'Invalid email or password')
}
} catch (error) {
console.error('Login error:', error);
setErrorMessage('Invalid email or password');
console.error('Login error:', error)
setErrorMessage('Network error. Please try again.')
} finally {
setIsLoading(false)
}
};
}
if (!isClient) {
return null;
const toggleTheme = () => {
const newTheme = theme === 'light' ? 'dark' : 'light'
setTheme(newTheme)
localStorage.setItem('theme', newTheme)
document.documentElement.classList.toggle('dark', newTheme === 'dark')
}
return (
<div className="login-page">
<div className="container">
<div className="doorman-logo">
Doorman
<div className="min-h-screen bg-gradient-to-br from-gray-50 via-blue-50 to-indigo-100 dark:from-dark-bg dark:via-gray-900 dark:to-gray-800 flex items-center justify-center p-4">
{/* Background decoration */}
<div className="absolute inset-0 overflow-hidden">
<div className="absolute -top-40 -right-40 h-80 w-80 rounded-full bg-gradient-to-br from-primary-400/20 to-primary-600/20 blur-3xl"></div>
<div className="absolute -bottom-40 -left-40 h-80 w-80 rounded-full bg-gradient-to-tr from-purple-400/20 to-purple-600/20 blur-3xl"></div>
</div>
{/* Theme toggle */}
<button
onClick={toggleTheme}
className="absolute top-6 right-6 rounded-lg p-2 text-gray-500 hover:bg-white/50 dark:text-gray-400 dark:hover:bg-dark-surface/50 transition-colors"
aria-label="Toggle theme"
>
{theme === 'light' ? (
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
</svg>
) : (
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
)}
</button>
<div className="relative w-full max-w-md">
{/* Logo and title */}
<div className="text-center mb-8">
<div className="mx-auto mb-4 h-16 w-16 rounded-2xl bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center shadow-lg">
<span className="text-white font-bold text-2xl">D</span>
</div>
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-2">
Welcome back
</h1>
<p className="text-gray-600 dark:text-gray-400">
Sign in to your Doorman account
</p>
</div>
<div className="content">
<div className="copy">
<h1 className="title">Login</h1>
<p className="enter-your-email-and-password">
Enter your email and password
{/* Login form */}
<div className="card animate-fade-in">
<form onSubmit={handleLogin} className="space-y-6">
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Email address
</label>
<input
id="email"
type="email"
required
className="input"
placeholder="Enter your email"
value={email}
onChange={(e) => setEmail(e.target.value)}
disabled={isLoading}
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Password
</label>
<input
id="password"
type="password"
required
className="input"
placeholder="Enter your password"
value={password}
onChange={(e) => setPassword(e.target.value)}
disabled={isLoading}
/>
</div>
{errorMessage && (
<div className="rounded-lg bg-error-50 border border-error-200 p-4 dark:bg-error-900/20 dark:border-error-800">
<div className="flex">
<svg className="h-5 w-5 text-error-400 dark:text-error-500 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div className="ml-3">
<p className="text-sm text-error-700 dark:text-error-300">{errorMessage}</p>
</div>
</div>
</div>
)}
<button
type="submit"
disabled={isLoading}
className="btn btn-primary w-full py-3 text-base font-medium"
>
{isLoading ? (
<div className="flex items-center justify-center">
<div className="spinner mr-2"></div>
Signing in...
</div>
) : (
'Sign in'
)}
</button>
</form>
<div className="mt-6 text-center">
<p className="text-xs text-gray-500 dark:text-gray-400">
By signing in, you agree to our{' '}
<a href="/terms" className="text-primary-600 hover:text-primary-500 dark:text-primary-400 dark:hover:text-primary-300 font-medium">
Terms of Service
</a>{' '}
and{' '}
<a href="/privacy" className="text-primary-600 hover:text-primary-500 dark:text-primary-400 dark:hover:text-primary-300 font-medium">
Privacy Policy
</a>
</p>
</div>
<div className="input-and-button">
<form onSubmit={(e) => e.preventDefault()}>
<div className="email-field">
<input
type="email"
className="email"
placeholder="email@domain.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<div className="password-field">
<input
type="password"
className="password"
placeholder="••••••"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<button className="button" onClick={handleLogin}>Login</button>
</form>
</div>
{errorMessage && <div className="error-message">{errorMessage}</div>}
<p className="terms">
By clicking Login, you agree to our{" "}
<a href="/terms" className="link">Terms of Service</a> and{" "}
<a href="/privacy" className="link">Privacy Policy</a>.
</div>
{/* Footer */}
<div className="mt-8 text-center">
<p className="text-sm text-gray-500 dark:text-gray-400">
Need help?{' '}
<a href="/support" className="text-primary-600 hover:text-primary-500 dark:text-primary-400 dark:hover:text-primary-300 font-medium">
Contact support
</a>
</p>
</div>
</div>
</div>
);
};
)
}
export default LoginPage;
export default LoginPage
+189 -210
View File
@@ -1,260 +1,239 @@
'use client'
import React, { useState, useEffect } from 'react';
import Link from 'next/link';
import './monitor.css';
import React, { useState, useEffect } from 'react'
import Layout from '@/components/Layout'
interface Metric {
timestamp: string;
value: number;
timestamp: string
value: number
}
interface Metrics {
totalRequests: Metric[];
errorRate: Metric[];
avgResponseTime: Metric[];
activeUsers: Metric[];
bandwidthUsage: Metric[];
cpuUsage: Metric[];
memoryUsage: Metric[];
totalRequests: Metric[]
errorRate: Metric[]
avgResponseTime: Metric[]
activeUsers: Metric[]
bandwidthUsage: Metric[]
cpuUsage: Metric[]
memoryUsage: Metric[]
statusCodes: {
[key: string]: number;
};
[key: string]: number
}
}
const menuItems = [
{ label: 'Dashboard', href: '/dashboard' },
{ label: 'APIs', href: '/apis' },
{ label: 'Routings', href: '/routings' },
{ label: 'Users', href: '/users' },
{ label: 'Groups', href: '/groups' },
{ label: 'Roles', href: '/roles' },
{ label: 'Monitor', href: '/monitor' },
{ label: 'Logs', href: '/logging' },
{ label: 'Security', href: '/security' },
{ label: 'Settings', href: '/settings' },
];
const MonitorPage: React.FC = () => {
const [theme, setTheme] = useState('light');
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [metrics, setMetrics] = useState<any[]>([]);
const [timeRange, setTimeRange] = useState('24h');
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [metrics, setMetrics] = useState<any[]>([])
const [timeRange, setTimeRange] = useState('24h')
useEffect(() => {
const savedTheme = localStorage.getItem('theme') || 'light';
setTheme(savedTheme);
document.documentElement.setAttribute('data-theme', savedTheme);
}, []);
useEffect(() => {
fetchMetrics();
}, [timeRange]);
fetchMetrics()
}, [timeRange])
const fetchMetrics = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch(`/api/platform/metrics?range=${timeRange}`);
setLoading(true)
setError(null)
const response = await fetch(`/api/platform/metrics?range=${timeRange}`)
if (!response.ok) {
throw new Error('Failed to fetch metrics');
throw new Error('Failed to fetch metrics')
}
const data = await response.json();
setMetrics(data);
const data = await response.json()
setMetrics(data)
} catch (err) {
if (err instanceof Error) {
setError(err.message);
setError(err.message)
} else {
setError('An unknown error occurred');
setError('An unknown error occurred')
}
setMetrics([]);
setMetrics([])
} finally {
setLoading(false);
setLoading(false)
}
};
const handleLogout = () => {
localStorage.clear();
sessionStorage.clear();
setTimeout(() => {
window.location.replace('/');
}, 50);
};
}
const renderMetricChart = (data: Metric[], title: string): React.ReactNode => {
return (
<div className="monitor-metric-chart">
<div className="monitor-chart-placeholder">
{title} chart will be implemented here
<div className="card">
<div className="card-header">
<h3 className="card-title">{title}</h3>
</div>
<div className="p-6">
<div className="h-48 bg-gray-50 dark:bg-gray-800 rounded-lg flex items-center justify-center">
<p className="text-gray-500 dark:text-gray-400">{title} chart will be implemented here</p>
</div>
</div>
</div>
);
};
)
}
return (
<>
<div className="monitor-topbar">
Doorman
</div>
<div className="monitor-root">
<aside className="monitor-sidebar">
<div className="monitor-sidebar-title">Menu</div>
<ul className="monitor-sidebar-list">
{menuItems.map((item, idx) => (
item.href ? (
<li key={item.label} className={`monitor-sidebar-item${idx === 6 ? ' active' : ''}`}>
<Link href={item.href} style={{ color: 'inherit', textDecoration: 'none', display: 'block', width: '100%' }}>{item.label}</Link>
</li>
) : (
<li key={item.label} className={`monitor-sidebar-item${idx === 6 ? ' active' : ''}`}>{item.label}</li>
)
))}
</ul>
<button className="monitor-logout-btn" onClick={handleLogout}>
Logout
</button>
</aside>
<main className="monitor-main">
<div className="monitor-header-row">
<h1 className="monitor-title">Monitor</h1>
<button className="monitor-refresh-button" onClick={fetchMetrics}>
<span className="monitor-refresh-icon"></span>
Refresh
</button>
<Layout>
<div className="space-y-6">
{/* Page Header */}
<div className="page-header">
<div>
<h1 className="page-title">Monitor</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">
Real-time system metrics and performance monitoring
</p>
</div>
<div className="monitor-controls">
<select
value={timeRange}
<div className="flex gap-2">
<select
value={timeRange}
onChange={(e) => setTimeRange(e.target.value)}
className="monitor-time-range-select"
className="input"
>
<option value="1h">Last Hour</option>
<option value="6h">Last 6 Hours</option>
<option value="24h">Last 24 Hours</option>
<option value="7d">Last 7 Days</option>
<option value="30d">Last 30 Days</option>
</select>
<button onClick={fetchMetrics} className="btn btn-secondary">
<svg className="h-4 w-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
Refresh
</button>
</div>
</div>
{error && (
<div className="monitor-error-message">
{error}
{/* Error Message */}
{error && (
<div className="rounded-lg bg-error-50 border border-error-200 p-4 dark:bg-error-900/20 dark:border-error-800">
<div className="flex">
<svg className="h-5 w-5 text-error-400 dark:text-error-500 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div className="ml-3">
<p className="text-sm text-error-700 dark:text-error-300">{error}</p>
</div>
</div>
)}
</div>
)}
{loading ? (
<div className="monitor-loading-spinner">
<div className="monitor-spinner"></div>
<p>Loading metrics...</p>
{/* Loading State */}
{loading ? (
<div className="card">
<div className="flex items-center justify-center py-12">
<div className="text-center">
<div className="spinner mx-auto mb-4"></div>
<p className="text-gray-600 dark:text-gray-400">Loading metrics...</p>
</div>
</div>
) : (
<div className="monitor-metrics-grid">
<div className="monitor-metric-card">
<h3>Total Requests</h3>
<div className="monitor-metric-value">
{metrics.length > 0
? metrics.reduce((sum, m) => sum + m.requests, 0).toLocaleString()
: '0'
}
</div>
) : (
/* Metrics Grid */
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Total Requests */}
<div className="stats-card">
<div className="flex items-center justify-between">
<div>
<p className="stats-label">Total Requests</p>
<p className="stats-value">1,234,567</p>
<p className="stats-change positive">+12.5% from last period</p>
</div>
{renderMetricChart(
metrics.length > 0
? metrics.map(m => ({ timestamp: m.timestamp, value: m.requests }))
: [],
'Total Requests'
)}
</div>
<div className="monitor-metric-card">
<h3>Error Rate</h3>
<div className="monitor-metric-value">
{metrics.length > 0
? ((metrics.reduce((sum, m) => sum + m.errors, 0) /
metrics.reduce((sum, m) => sum + m.requests, 0)) * 100).toFixed(2)
: '0.00'
}%
</div>
{renderMetricChart(
metrics.length > 0
? metrics.map(m => ({ timestamp: m.timestamp, value: m.errors }))
: [],
'Error Rate'
)}
</div>
<div className="monitor-metric-card">
<h3>Average Response Time</h3>
<div className="monitor-metric-value">
{metrics.length > 0
? Math.round(metrics.reduce((sum, m) => sum + m.avgResponseTime, 0) / metrics.length)
: '0'
}ms
</div>
{renderMetricChart(
metrics.length > 0
? metrics.map(m => ({ timestamp: m.timestamp, value: m.avgResponseTime }))
: [],
'Average Response Time'
)}
</div>
<div className="monitor-metric-card">
<h3>Active Users</h3>
<div className="monitor-metric-value">
{metrics.length > 0
? Math.max(...metrics.map(m => m.activeUsers))
: '0'
}
</div>
{renderMetricChart(
metrics.length > 0
? metrics.map(m => ({ timestamp: m.timestamp, value: m.activeUsers }))
: [],
'Active Users'
)}
</div>
<div className="monitor-metric-card">
<h3>Bandwidth Usage</h3>
<div className="monitor-metric-value">
{metrics.length > 0
? (metrics.reduce((sum, m) => sum + m.bandwidth, 0) / 1000000).toFixed(2)
: '0.00'
} MB
</div>
{renderMetricChart(
metrics.length > 0
? metrics.map(m => ({ timestamp: m.timestamp, value: m.bandwidth }))
: [],
'Bandwidth Usage'
)}
</div>
<div className="monitor-metric-card wide">
<h3>Status Code Distribution</h3>
<div className="monitor-status-codes">
{metrics.length > 0 ? (
Object.entries(metrics[metrics.length - 1].statusCodes).map(([code, count]) => (
<div key={code} className="monitor-status-code-item">
<span className="monitor-status-code-label">{code}</span>
<span className="monitor-status-code-count">{Number(count)}</span>
</div>
))
) : (
<div className="monitor-status-code-item">
<span className="monitor-status-code-label">No data</span>
<span className="monitor-status-code-count">0</span>
</div>
)}
<div className="h-12 w-12 rounded-lg bg-blue-100 dark:bg-blue-900/20 flex items-center justify-center">
<svg className="h-6 w-6 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
</div>
</div>
</div>
)}
</main>
{/* Error Rate */}
<div className="stats-card">
<div className="flex items-center justify-between">
<div>
<p className="stats-label">Error Rate</p>
<p className="stats-value">0.23%</p>
<p className="stats-change negative">+0.05% from last period</p>
</div>
<div className="h-12 w-12 rounded-lg bg-red-100 dark:bg-red-900/20 flex items-center justify-center">
<svg className="h-6 w-6 text-red-600 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
</div>
</div>
{/* Average Response Time */}
<div className="stats-card">
<div className="flex items-center justify-between">
<div>
<p className="stats-label">Avg Response Time</p>
<p className="stats-value">142ms</p>
<p className="stats-change positive">-8ms from last period</p>
</div>
<div className="h-12 w-12 rounded-lg bg-green-100 dark:bg-green-900/20 flex items-center justify-center">
<svg className="h-6 w-6 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
</div>
</div>
</div>
{/* Active Users */}
<div className="stats-card">
<div className="flex items-center justify-between">
<div>
<p className="stats-label">Active Users</p>
<p className="stats-value">847</p>
<p className="stats-change positive">+23 from last period</p>
</div>
<div className="h-12 w-12 rounded-lg bg-purple-100 dark:bg-purple-900/20 flex items-center justify-center">
<svg className="h-6 w-6 text-purple-600 dark:text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
</svg>
</div>
</div>
</div>
</div>
)}
{/* Charts Section */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{renderMetricChart([], 'Request Volume')}
{renderMetricChart([], 'Response Times')}
{renderMetricChart([], 'Error Rates')}
{renderMetricChart([], 'Bandwidth Usage')}
</div>
{/* System Status */}
<div className="card">
<div className="card-header">
<h3 className="card-title">System Status</h3>
</div>
<div className="p-6">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="flex items-center p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
<div className="h-3 w-3 bg-green-500 rounded-full mr-3"></div>
<div>
<p className="font-medium text-green-900 dark:text-green-100">API Gateway</p>
<p className="text-sm text-green-600 dark:text-green-400">Operational</p>
</div>
</div>
<div className="flex items-center p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
<div className="h-3 w-3 bg-green-500 rounded-full mr-3"></div>
<div>
<p className="font-medium text-green-900 dark:text-green-100">Database</p>
<p className="text-sm text-green-600 dark:text-green-400">Operational</p>
</div>
</div>
<div className="flex items-center p-4 bg-green-50 dark:bg-green-900/20 rounded-lg">
<div className="h-3 w-3 bg-green-500 rounded-full mr-3"></div>
<div>
<p className="font-medium text-green-900 dark:text-green-100">Cache</p>
<p className="text-sm text-green-600 dark:text-green-400">Operational</p>
</div>
</div>
</div>
</div>
</div>
</div>
</>
);
};
</Layout>
)
}
export default MonitorPage;
export default MonitorPage
+346 -318
View File
@@ -1,86 +1,59 @@
'use client'
import React, { useState, useEffect } from 'react';
import Link from 'next/link';
import { useRouter, useParams } from 'next/navigation';
import '../roles.css';
import React, { useState, useEffect } from 'react'
import Link from 'next/link'
import { useRouter, useParams } from 'next/navigation'
import Layout from '@/components/Layout'
interface Role {
role_name: string;
role_description: string;
manage_users?: boolean;
manage_apis?: boolean;
manage_endpoints?: boolean;
manage_groups?: boolean;
manage_roles?: boolean;
manage_routings?: boolean;
manage_gateway?: boolean;
manage_subscriptions?: boolean;
view_logs?: boolean;
export_logs?: boolean;
role_name: string
role_description: string
manage_users?: boolean
manage_apis?: boolean
manage_endpoints?: boolean
manage_groups?: boolean
manage_roles?: boolean
manage_routings?: boolean
manage_gateway?: boolean
manage_subscriptions?: boolean
view_logs?: boolean
export_logs?: boolean
}
const menuItems = [
{ label: 'Dashboard', href: '/dashboard' },
{ label: 'APIs', href: '/apis' },
{ label: 'Routings', href: '/routings' },
{ label: 'Users', href: '/users' },
{ label: 'Groups', href: '/groups' },
{ label: 'Roles', href: '/roles' },
{ label: 'Monitor', href: '/monitor' },
{ label: 'Logs', href: '/logging' },
{ label: 'Security', href: '/security' },
{ label: 'Settings', href: '/settings' },
];
const handleLogout = () => {
localStorage.clear();
sessionStorage.clear();
setTimeout(() => {
window.location.replace('/');
}, 50);
};
const RoleDetailPage = () => {
const router = useRouter();
const params = useParams();
const roleName = params.roleName as string;
const router = useRouter()
const params = useParams()
const roleName = params.roleName as string
const [theme, setTheme] = useState('light');
const [role, setRole] = useState<Role | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [isEditing, setIsEditing] = useState(false);
const [editData, setEditData] = useState<Partial<Role>>({});
const [saving, setSaving] = useState(false);
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [deleting, setDeleting] = useState(false);
const [deleteConfirmation, setDeleteConfirmation] = useState('');
const [role, setRole] = useState<Role | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [success, setSuccess] = useState<string | null>(null)
const [isEditing, setIsEditing] = useState(false)
const [editData, setEditData] = useState<Partial<Role>>({})
const [saving, setSaving] = useState(false)
const [showDeleteModal, setShowDeleteModal] = useState(false)
const [deleting, setDeleting] = useState(false)
const [deleteConfirmation, setDeleteConfirmation] = useState('')
useEffect(() => {
const savedTheme = localStorage.getItem('theme') || 'light';
setTheme(savedTheme);
document.documentElement.setAttribute('data-theme', savedTheme);
}, []);
useEffect(() => {
fetchRole();
}, [roleName]);
fetchRole()
}, [roleName])
const fetchRole = async () => {
try {
setLoading(true);
setError(null);
setLoading(true)
setError(null)
// Try to get from sessionStorage first
const savedRole = sessionStorage.getItem('selectedRole');
const savedRole = sessionStorage.getItem('selectedRole')
if (savedRole) {
const parsedRole = JSON.parse(savedRole);
const parsedRole = JSON.parse(savedRole)
if (parsedRole.role_name === roleName) {
setRole(parsedRole);
setEditData(parsedRole);
setLoading(false);
return;
setRole(parsedRole)
setEditData(parsedRole)
setLoading(false)
return
}
}
@@ -92,35 +65,39 @@ const RoleDetailPage = () => {
'Content-Type': 'application/json',
'Cookie': `access_token_cookie=${document.cookie.split('; ').find(row => row.startsWith('access_token_cookie='))?.split('=')[1]}`
}
});
})
if (!response.ok) {
throw new Error('Failed to load role');
throw new Error('Failed to load role')
}
const data = await response.json();
setRole(data);
setEditData(data);
const data = await response.json()
setRole(data)
setEditData(data)
} catch (err) {
setError('Failed to load role. Please try again later.');
setError('Failed to load role. Please try again later.')
} finally {
setLoading(false);
setLoading(false)
}
};
}
const handleBack = () => {
router.push('/roles')
}
const handleEdit = () => {
setIsEditing(true);
};
setIsEditing(true)
}
const handleCancel = () => {
setIsEditing(false);
setEditData(role || {});
};
setIsEditing(false)
setEditData(role || {})
}
const handleSave = async () => {
try {
setSaving(true);
setError(null);
setSaving(true)
setError(null)
const response = await fetch(`http://localhost:3002/platform/role/${encodeURIComponent(roleName)}`, {
method: 'PUT',
@@ -131,26 +108,38 @@ const RoleDetailPage = () => {
'Cookie': `access_token_cookie=${document.cookie.split('; ').find(row => row.startsWith('access_token_cookie='))?.split('=')[1]}`
},
body: JSON.stringify(editData)
});
})
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error_message || 'Failed to update role');
const errorData = await response.json()
throw new Error(errorData.error_message || 'Failed to update role')
}
setRole({ ...role, ...editData } as Role);
setIsEditing(false);
const updatedRole = await response.json()
setRole(updatedRole)
setIsEditing(false)
setSuccess('Role updated successfully!')
setTimeout(() => setSuccess(null), 3000)
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to update role. Please try again later.');
if (err instanceof Error) {
setError(err.message)
} else {
setError('Failed to update role. Please try again later.')
}
} finally {
setSaving(false);
setSaving(false)
}
};
}
const handleDelete = async () => {
if (deleteConfirmation !== role?.role_name) {
setError('Role name does not match')
return
}
try {
setDeleting(true);
setError(null);
setDeleting(true)
setError(null)
const response = await fetch(`http://localhost:3002/platform/role/${encodeURIComponent(roleName)}`, {
method: 'DELETE',
@@ -160,280 +149,319 @@ const RoleDetailPage = () => {
'Content-Type': 'application/json',
'Cookie': `access_token_cookie=${document.cookie.split('; ').find(row => row.startsWith('access_token_cookie='))?.split('=')[1]}`
}
});
})
if (!response.ok) {
throw new Error('Failed to delete role');
throw new Error('Failed to delete role')
}
router.push('/roles');
router.push('/roles')
} catch (err) {
setError('Failed to delete role. Please try again later.');
setShowDeleteModal(false);
setError('Failed to delete role. Please try again later.')
setShowDeleteModal(false)
} finally {
setDeleting(false);
setDeleting(false)
}
};
}
const handleInputChange = (field: keyof Role, value: any) => {
setEditData(prev => ({ ...prev, [field]: value }));
};
setEditData(prev => ({ ...prev, [field]: value }))
}
const handlePermissionChange = (permission: keyof Role, value: boolean) => {
setEditData(prev => ({ ...prev, [permission]: value }));
};
setEditData(prev => ({ ...prev, [permission]: value }))
}
const getPermissionCount = (roleData: Role) => {
return Object.values(roleData).filter(val => typeof val === 'boolean' && val).length;
};
return Object.values(roleData).filter(val => typeof val === 'boolean' && val).length
}
if (loading) {
return (
<>
<div className="roles-topbar">Doorman</div>
<div className="roles-root">
<aside className="roles-sidebar">
<div className="roles-sidebar-title">Menu</div>
<ul className="roles-sidebar-list">
{menuItems.map((item, idx) => (
item.href ? (
<li key={item.label} className={`roles-sidebar-item${idx === 5 ? ' active' : ''}`}>
<Link href={item.href} style={{ color: 'inherit', textDecoration: 'none', display: 'block', width: '100%' }}>{item.label}</Link>
</li>
) : (
<li key={item.label} className={`roles-sidebar-item${idx === 5 ? ' active' : ''}`}>{item.label}</li>
)
))}
</ul>
<button className="roles-logout-btn" onClick={handleLogout}>Logout</button>
</aside>
<main className="roles-main">
<div className="loading-spinner">
<div className="spinner"></div>
<p>Loading role...</p>
</div>
</main>
<Layout>
<div className="flex items-center justify-center py-12">
<div className="text-center">
<div className="spinner mx-auto mb-4"></div>
<p className="text-gray-600 dark:text-gray-400">Loading role details...</p>
</div>
</div>
</>
);
</Layout>
)
}
if (!role) {
if (error && !role) {
return (
<>
<div className="roles-topbar">Doorman</div>
<div className="roles-root">
<aside className="roles-sidebar">
<div className="roles-sidebar-title">Menu</div>
<ul className="roles-sidebar-list">
{menuItems.map((item, idx) => (
item.href ? (
<li key={item.label} className={`roles-sidebar-item${idx === 5 ? ' active' : ''}`}>
<Link href={item.href} style={{ color: 'inherit', textDecoration: 'none', display: 'block', width: '100%' }}>{item.label}</Link>
</li>
) : (
<li key={item.label} className={`roles-sidebar-item${idx === 5 ? ' active' : ''}`}>{item.label}</li>
)
))}
</ul>
<button className="roles-logout-btn" onClick={handleLogout}>Logout</button>
</aside>
<main className="roles-main">
<div className="error-container">
<div className="error-message">Role not found</div>
<Link href="/roles" className="back-link">Back to Roles</Link>
<Layout>
<div className="space-y-6">
<div className="page-header">
<div>
<h1 className="page-title">Role Details</h1>
</div>
</main>
<button onClick={handleBack} className="btn btn-secondary">
<svg className="h-4 w-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
Back to Roles
</button>
</div>
<div className="rounded-lg bg-error-50 border border-error-200 p-4 dark:bg-error-900/20 dark:border-error-800">
<div className="flex">
<svg className="h-5 w-5 text-error-400 dark:text-error-500 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div className="ml-3">
<p className="text-sm text-error-700 dark:text-error-300">{error}</p>
</div>
</div>
</div>
</div>
</>
);
</Layout>
)
}
return (
<>
<div className="roles-topbar">Doorman</div>
<div className="roles-root">
<aside className="roles-sidebar">
<div className="roles-sidebar-title">Menu</div>
<ul className="roles-sidebar-list">
{menuItems.map((item, idx) => (
item.href ? (
<li key={item.label} className={`roles-sidebar-item${idx === 5 ? ' active' : ''}`}>
<Link href={item.href} style={{ color: 'inherit', textDecoration: 'none', display: 'block', width: '100%' }}>{item.label}</Link>
</li>
) : (
<li key={item.label} className={`roles-sidebar-item${idx === 5 ? ' active' : ''}`}>{item.label}</li>
)
))}
</ul>
<button className="roles-logout-btn" onClick={handleLogout}>Logout</button>
</aside>
<main className="roles-main">
<div className="roles-header">
<button className="back-button" onClick={() => router.push('/roles')}>
<span className="back-arrow"></span>
Back to Roles
</button>
<h1 className="roles-title">Role Details</h1>
<Layout>
<div className="space-y-6">
{/* Page Header */}
<div className="page-header">
<div>
<h1 className="page-title">{role?.role_name || 'Role Details'}</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">
Manage role permissions and settings
</p>
</div>
<div className="flex gap-2">
{!isEditing ? (
<button className="edit-button" onClick={handleEdit}>
Edit Role
</button>
) : (
<div className="edit-actions">
<button className="save-button" onClick={handleSave} disabled={saving}>
{saving ? 'Saving...' : 'Save Changes'}
<>
<button onClick={handleEdit} className="btn btn-primary">
<svg className="h-4 w-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
Edit Role
</button>
<button className="cancel-button" onClick={handleCancel} disabled={saving}>
Cancel
</button>
<button className="delete-button" onClick={() => setShowDeleteModal(true)} disabled={saving}>
<button onClick={() => setShowDeleteModal(true)} className="btn btn-error">
<svg className="h-4 w-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
Delete Role
</button>
</div>
)}
</div>
{error && (
<div className="error-container">
<div className="error-message">{error}</div>
</div>
)}
<div className="roles-detail-content">
<div className="roles-detail-card">
<div className="roles-detail-section">
<h2 className="section-title">Basic Information</h2>
<div className="info-grid">
<div className="info-item">
<label className="info-label">Role Name</label>
{isEditing ? (
<input
type="text"
className="edit-input"
value={editData.role_name || ''}
onChange={(e) => handleInputChange('role_name', e.target.value)}
disabled={saving}
/>
) : (
<span className="info-value">{role.role_name}</span>
)}
</div>
<div className="info-item">
<label className="info-label">Description</label>
{isEditing ? (
<textarea
className="edit-input"
value={editData.role_description || ''}
onChange={(e) => handleInputChange('role_description', e.target.value)}
disabled={saving}
rows={3}
/>
) : (
<span className="info-value">{role.role_description || 'No description'}</span>
)}
</div>
</div>
</div>
<div className="roles-detail-section">
<h2 className="section-title">Permissions</h2>
<div className="permissions-grid">
{[
{ key: 'manage_users', label: 'Manage Users' },
{ key: 'manage_apis', label: 'Manage APIs' },
{ key: 'manage_endpoints', label: 'Manage Endpoints' },
{ key: 'manage_groups', label: 'Manage Groups' },
{ key: 'manage_roles', label: 'Manage Roles' },
{ key: 'manage_routings', label: 'Manage Routings' },
{ key: 'manage_gateway', label: 'Manage Gateway' },
{ key: 'manage_subscriptions', label: 'Manage Subscriptions' },
{ key: 'view_logs', label: 'View Logs' },
{ key: 'export_logs', label: 'Export Logs' }
].map(({ key, label }) => (
<div key={key} className="permission-item">
{isEditing ? (
<label className="permission-label">
<input
type="checkbox"
checked={editData[key as keyof Role] as boolean || false}
onChange={(e) => handlePermissionChange(key as keyof Role, e.target.checked)}
disabled={saving}
/>
<span className="permission-text">{label}</span>
</label>
) : (
<div className="permission-display">
<span className={`permission-badge ${(role[key as keyof Role] as boolean) ? 'enabled' : 'disabled'}`}>
{(role[key as keyof Role] as boolean) ? '✓' : '✗'} {label}
</span>
</div>
)}
</>
) : (
<>
<button onClick={handleSave} disabled={saving} className="btn btn-primary">
{saving ? (
<div className="flex items-center">
<div className="spinner mr-2"></div>
Saving...
</div>
))}
) : (
<>
<svg className="h-4 w-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
Save Changes
</>
)}
</button>
<button onClick={handleCancel} className="btn btn-secondary">
Cancel
</button>
</>
)}
<button onClick={handleBack} className="btn btn-ghost">
<svg className="h-4 w-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
Back
</button>
</div>
</div>
{/* Success Message */}
{success && (
<div className="rounded-lg bg-success-50 border border-success-200 p-4 dark:bg-success-900/20 dark:border-success-800">
<div className="flex">
<svg className="h-5 w-5 text-success-400 dark:text-success-500 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div className="ml-3">
<p className="text-sm text-success-700 dark:text-success-300">{success}</p>
</div>
</div>
</div>
)}
{/* Error Message */}
{error && (
<div className="rounded-lg bg-error-50 border border-error-200 p-4 dark:bg-error-900/20 dark:border-error-800">
<div className="flex">
<svg className="h-5 w-5 text-error-400 dark:text-error-500 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div className="ml-3">
<p className="text-sm text-error-700 dark:text-error-300">{error}</p>
</div>
</div>
</div>
)}
{role && (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Basic Information */}
<div className="card">
<div className="card-header">
<h3 className="card-title">Basic Information</h3>
</div>
<div className="p-6 space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Role Name
</label>
{isEditing ? (
<input
type="text"
value={editData.role_name || ''}
onChange={(e) => handleInputChange('role_name', e.target.value)}
className="input"
placeholder="Enter role name"
/>
) : (
<p className="text-gray-900 dark:text-white">{role.role_name}</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Description
</label>
{isEditing ? (
<textarea
value={editData.role_description || ''}
onChange={(e) => handleInputChange('role_description', e.target.value)}
className="input resize-none"
rows={3}
placeholder="Enter role description"
/>
) : (
<p className="text-gray-600 dark:text-gray-400">{role.role_description || 'No description'}</p>
)}
</div>
{!isEditing && (
<div className="permissions-summary">
<span className="permission-badge">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Total Permissions
</label>
<span className="badge badge-primary">
{getPermissionCount(role)} permission{getPermissionCount(role) !== 1 ? 's' : ''} enabled
</span>
</div>
)}
</div>
</div>
</div>
</main>
</div>
{/* Delete Confirmation Modal */}
{showDeleteModal && (
<div className="modal-overlay">
<div className="modal-content">
<div className="modal-header">
<h2 className="modal-title">Delete Role</h2>
<button className="modal-close" onClick={() => setShowDeleteModal(false)}>
×
</button>
{/* Permissions */}
<div className="card">
<div className="card-header">
<h3 className="card-title">Permissions</h3>
</div>
<div className="p-6 space-y-4">
<div className="grid grid-cols-1 gap-3">
{[
{ key: 'manage_users', label: 'Manage Users', description: 'Create, edit, and delete user accounts' },
{ key: 'manage_apis', label: 'Manage APIs', description: 'Create, edit, and delete API configurations' },
{ key: 'manage_endpoints', label: 'Manage Endpoints', description: 'Configure API endpoints and validations' },
{ key: 'manage_groups', label: 'Manage Groups', description: 'Create, edit, and delete user groups' },
{ key: 'manage_roles', label: 'Manage Roles', description: 'Create, edit, and delete user roles' },
{ key: 'manage_routings', label: 'Manage Routings', description: 'Configure API routing and load balancing' },
{ key: 'manage_gateway', label: 'Manage Gateway', description: 'Configure gateway settings and policies' },
{ key: 'manage_subscriptions', label: 'Manage Subscriptions', description: 'Manage API subscriptions and billing' },
{ key: 'view_logs', label: 'View Logs', description: 'View system logs and API requests' },
{ key: 'export_logs', label: 'Export Logs', description: 'Export logs in various formats' }
].map(({ key, label, description }) => (
<div key={key} className="flex items-start space-x-3">
{isEditing ? (
<input
type="checkbox"
id={key}
checked={editData[key as keyof Role] as boolean || false}
onChange={(e) => handlePermissionChange(key as keyof Role, e.target.checked)}
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded mt-1"
disabled={saving}
/>
) : (
<div className={`h-4 w-4 rounded mt-1 flex items-center justify-center ${
(role[key as keyof Role] as boolean)
? 'bg-success-500 text-white'
: 'bg-gray-300 dark:bg-gray-600'
}`}>
{(role[key as keyof Role] as boolean) && (
<svg className="h-3 w-3" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
)}
</div>
)}
<div className="flex-1">
<label htmlFor={key} className={`block text-sm font-medium ${isEditing ? 'text-gray-700 dark:text-gray-300 cursor-pointer' : 'text-gray-900 dark:text-white'}`}>
{label}
</label>
<p className="text-xs text-gray-500 dark:text-gray-400">
{description}
</p>
</div>
</div>
))}
</div>
</div>
</div>
<div className="modal-body">
<p className="modal-message">
This action cannot be undone. This will permanently delete the role <strong>{role?.role_name}</strong>.
</div>
)}
{/* Delete Modal */}
{showDeleteModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center">
<div className="fixed inset-0 bg-black/50" onClick={() => setShowDeleteModal(false)}></div>
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-md w-full mx-4 relative z-10">
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-4">Delete Role</h3>
<p className="text-gray-600 dark:text-gray-400 mb-4">
This action cannot be undone. This will permanently delete the role "{role?.role_name}".
</p>
<p className="modal-warning">
To confirm deletion, please type the role name <strong>{role?.role_name}</strong> in the field below:
<p className="text-sm text-gray-500 dark:text-gray-400 mb-4">
Please type <strong>{role?.role_name}</strong> to confirm.
</p>
<input
type="text"
className="modal-input"
placeholder="Enter role name to confirm"
value={deleteConfirmation}
onChange={(e) => setDeleteConfirmation(e.target.value)}
autoFocus
className="input w-full mb-4"
placeholder="Enter role name to confirm"
/>
</div>
<div className="modal-footer">
<button
className="modal-cancel-button"
onClick={() => setShowDeleteModal(false)}
disabled={deleting}
>
Cancel
</button>
<button
className="modal-delete-button"
onClick={handleDelete}
disabled={deleting || deleteConfirmation !== role?.role_name}
>
{deleting ? 'Deleting...' : 'Delete Role'}
</button>
<div className="flex gap-2">
<button
onClick={handleDelete}
disabled={deleteConfirmation !== role?.role_name || deleting}
className="btn btn-error flex-1"
>
{deleting ? (
<div className="flex items-center justify-center">
<div className="spinner mr-2"></div>
Deleting...
</div>
) : (
'Delete Role'
)}
</button>
<button onClick={() => setShowDeleteModal(false)} className="btn btn-secondary flex-1">
Cancel
</button>
</div>
</div>
</div>
</div>
)}
</>
);
};
)}
</div>
</Layout>
)
}
export default RoleDetailPage;
export default RoleDetailPage
+162 -230
View File
@@ -1,49 +1,27 @@
'use client'
import React, { useState, useEffect } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import '../roles.css';
import React, { useState } from 'react'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
import Layout from '@/components/Layout'
interface CreateRoleData {
role_name: string;
role_description: string;
manage_users: boolean;
manage_apis: boolean;
manage_endpoints: boolean;
manage_groups: boolean;
manage_roles: boolean;
manage_routings: boolean;
manage_gateway: boolean;
manage_subscriptions: boolean;
role_name: string
role_description: string
manage_users: boolean
manage_apis: boolean
manage_endpoints: boolean
manage_groups: boolean
manage_roles: boolean
manage_routings: boolean
manage_gateway: boolean
manage_subscriptions: boolean
}
const menuItems = [
{ label: 'Dashboard', href: '/dashboard' },
{ label: 'APIs', href: '/apis' },
{ label: 'Routings', href: '/routings' },
{ label: 'Users', href: '/users' },
{ label: 'Groups', href: '/groups' },
{ label: 'Roles', href: '/roles' },
{ label: 'Monitor', href: '/monitor' },
{ label: 'Logs', href: '/logging' },
{ label: 'Security', href: '/security' },
{ label: 'Settings', href: '/settings' },
];
const handleLogout = () => {
localStorage.clear();
sessionStorage.clear();
setTimeout(() => {
window.location.replace('/');
}, 50);
};
const AddRolePage = () => {
const router = useRouter();
const [theme, setTheme] = useState('light');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const router = useRouter()
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [formData, setFormData] = useState<CreateRoleData>({
role_name: '',
role_description: '',
@@ -55,29 +33,23 @@ const AddRolePage = () => {
manage_routings: false,
manage_gateway: false,
manage_subscriptions: false
});
useEffect(() => {
const savedTheme = localStorage.getItem('theme') || 'light';
setTheme(savedTheme);
document.documentElement.setAttribute('data-theme', savedTheme);
}, []);
})
const handleInputChange = (field: keyof CreateRoleData, value: any) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
setFormData(prev => ({ ...prev, [field]: value }))
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
e.preventDefault()
if (!formData.role_name.trim()) {
setError('Role name is required');
return;
setError('Role name is required')
return
}
try {
setLoading(true);
setError(null);
setLoading(true)
setError(null)
const response = await fetch('http://localhost:3002/platform/role', {
method: 'POST',
@@ -88,195 +60,155 @@ const AddRolePage = () => {
'Cookie': `access_token_cookie=${document.cookie.split('; ').find(row => row.startsWith('access_token_cookie='))?.split('=')[1]}`
},
body: JSON.stringify(formData)
});
})
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error_message || 'Failed to create role');
const errorData = await response.json()
throw new Error(errorData.detail || 'Failed to create role')
}
// Redirect back to roles list after successful creation
router.push('/roles');
router.push('/roles')
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to create role');
if (err instanceof Error) {
setError(err.message)
} else {
setError('Failed to create role. Please try again.')
}
} finally {
setLoading(false);
setLoading(false)
}
};
}
const permissions = [
{ key: 'manage_users', label: 'Manage Users', description: 'Create, edit, and delete user accounts' },
{ key: 'manage_apis', label: 'Manage APIs', description: 'Create, edit, and delete API configurations' },
{ key: 'manage_endpoints', label: 'Manage Endpoints', description: 'Configure API endpoints and validations' },
{ key: 'manage_groups', label: 'Manage Groups', description: 'Create, edit, and delete user groups' },
{ key: 'manage_roles', label: 'Manage Roles', description: 'Create, edit, and delete user roles' },
{ key: 'manage_routings', label: 'Manage Routings', description: 'Configure API routing and load balancing' },
{ key: 'manage_gateway', label: 'Manage Gateway', description: 'Configure gateway settings and policies' },
{ key: 'manage_subscriptions', label: 'Manage Subscriptions', description: 'Manage API subscriptions and billing' }
]
return (
<>
<div className="roles-topbar">
Doorman
</div>
<div className="roles-root">
<aside className="roles-sidebar">
<div className="roles-sidebar-title">Menu</div>
<ul className="roles-sidebar-list">
{menuItems.map((item, idx) => (
item.href ? (
<li key={item.label} className={`roles-sidebar-item${idx === 5 ? ' active' : ''}`}>
<Link href={item.href} style={{ color: 'inherit', textDecoration: 'none', display: 'block', width: '100%' }}>{item.label}</Link>
</li>
) : (
<li key={item.label} className={`roles-sidebar-item${idx === 5 ? ' active' : ''}`}>{item.label}</li>
)
))}
</ul>
<button className="roles-logout-btn" onClick={handleLogout}>
Logout
</button>
</aside>
<main className="roles-main">
<div className="roles-header-row">
<Link href="/roles" className="back-button" style={{ textDecoration: 'none', display: 'inline-block' }}>
<span className="back-arrow"></span>
Back to Roles
</Link>
<h1 className="roles-title">Add Role</h1>
<Layout>
<div className="space-y-6">
{/* Page Header */}
<div className="page-header">
<div>
<h1 className="page-title">Add Role</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">
Create a new user role with specific permissions
</p>
</div>
</div>
{error && (
<div className="error-container">
<div className="error-message">
{error}
{/* Error Message */}
{error && (
<div className="rounded-lg bg-error-50 border border-error-200 p-4 dark:bg-error-900/20 dark:border-error-800">
<div className="flex">
<svg className="h-5 w-5 text-error-400 dark:text-error-500 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div className="ml-3">
<p className="text-sm text-error-700 dark:text-error-300">{error}</p>
</div>
</div>
)}
<div className="add-role-form">
<form onSubmit={handleSubmit}>
<div className="form-section">
<h2 className="section-title">Basic Information</h2>
<div className="form-group">
<label className="form-label">Role Name *</label>
<input
type="text"
className="form-input"
value={formData.role_name}
onChange={(e) => handleInputChange('role_name', e.target.value)}
placeholder="Enter role name"
maxLength={50}
required
/>
</div>
<div className="form-group">
<label className="form-label">Description</label>
<textarea
className="form-input"
value={formData.role_description}
onChange={(e) => handleInputChange('role_description', e.target.value)}
placeholder="Enter role description"
maxLength={255}
rows={3}
/>
</div>
</div>
<div className="form-section">
<h2 className="section-title">Permissions</h2>
<div className="permissions-grid">
<div className="permission-item">
<label className="permission-label">
<input
type="checkbox"
checked={formData.manage_users}
onChange={(e) => handleInputChange('manage_users', e.target.checked)}
/>
<span className="permission-text">Manage Users</span>
</label>
</div>
<div className="permission-item">
<label className="permission-label">
<input
type="checkbox"
checked={formData.manage_apis}
onChange={(e) => handleInputChange('manage_apis', e.target.checked)}
/>
<span className="permission-text">Manage APIs</span>
</label>
</div>
<div className="permission-item">
<label className="permission-label">
<input
type="checkbox"
checked={formData.manage_endpoints}
onChange={(e) => handleInputChange('manage_endpoints', e.target.checked)}
/>
<span className="permission-text">Manage Endpoints</span>
</label>
</div>
<div className="permission-item">
<label className="permission-label">
<input
type="checkbox"
checked={formData.manage_groups}
onChange={(e) => handleInputChange('manage_groups', e.target.checked)}
/>
<span className="permission-text">Manage Groups</span>
</label>
</div>
<div className="permission-item">
<label className="permission-label">
<input
type="checkbox"
checked={formData.manage_roles}
onChange={(e) => handleInputChange('manage_roles', e.target.checked)}
/>
<span className="permission-text">Manage Roles</span>
</label>
</div>
<div className="permission-item">
<label className="permission-label">
<input
type="checkbox"
checked={formData.manage_routings}
onChange={(e) => handleInputChange('manage_routings', e.target.checked)}
/>
<span className="permission-text">Manage Routings</span>
</label>
</div>
<div className="permission-item">
<label className="permission-label">
<input
type="checkbox"
checked={formData.manage_gateway}
onChange={(e) => handleInputChange('manage_gateway', e.target.checked)}
/>
<span className="permission-text">Manage Gateway</span>
</label>
</div>
<div className="permission-item">
<label className="permission-label">
<input
type="checkbox"
checked={formData.manage_subscriptions}
onChange={(e) => handleInputChange('manage_subscriptions', e.target.checked)}
/>
<span className="permission-text">Manage Subscriptions</span>
</label>
</div>
</div>
</div>
<div className="form-actions">
<Link href="/roles" className="cancel-button" style={{ textDecoration: 'none', display: 'inline-block' }}>
Cancel
</Link>
<button
type="submit"
className="save-button"
disabled={loading}
>
{loading ? 'Creating...' : 'Create Role'}
</button>
</div>
</form>
</div>
</main>
</div>
</>
);
};
)}
export default AddRolePage;
{/* Form */}
<div className="card max-w-2xl">
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label htmlFor="role_name" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Role Name *
</label>
<input
type="text"
id="role_name"
name="role_name"
className="input"
placeholder="Enter role name"
value={formData.role_name}
onChange={(e) => handleInputChange('role_name', e.target.value)}
disabled={loading}
required
/>
</div>
<div>
<label htmlFor="role_description" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Description
</label>
<textarea
id="role_description"
name="role_description"
rows={4}
className="input resize-none"
placeholder="Describe the purpose of this role..."
value={formData.role_description}
onChange={(e) => handleInputChange('role_description', e.target.value)}
disabled={loading}
/>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
Optional description of the role's purpose
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-4">
Permissions
</label>
<div className="space-y-3">
{permissions.map((permission) => (
<div key={permission.key} className="flex items-start space-x-3">
<input
type="checkbox"
id={permission.key}
name={permission.key}
checked={formData[permission.key as keyof CreateRoleData] as boolean}
onChange={(e) => handleInputChange(permission.key as keyof CreateRoleData, e.target.checked)}
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded mt-1"
disabled={loading}
/>
<div className="flex-1">
<label htmlFor={permission.key} className="block text-sm font-medium text-gray-700 dark:text-gray-300 cursor-pointer">
{permission.label}
</label>
<p className="text-xs text-gray-500 dark:text-gray-400">
{permission.description}
</p>
</div>
</div>
))}
</div>
</div>
<div className="flex gap-4 pt-6 border-t border-gray-200 dark:border-gray-700">
<button
type="submit"
disabled={loading}
className="btn btn-primary flex-1"
>
{loading ? (
<div className="flex items-center justify-center">
<div className="spinner mr-2"></div>
Creating Role...
</div>
) : (
'Create Role'
)}
</button>
<Link href="/roles" className="btn btn-secondary flex-1">
Cancel
</Link>
</div>
</form>
</div>
</div>
</Layout>
)
}
export default AddRolePage
+175 -157
View File
@@ -1,68 +1,40 @@
'use client'
import React, { useState, useEffect } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import './roles.css';
import React, { useState, useEffect } from 'react'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
import Layout from '@/components/Layout'
interface Role {
role_name: string;
role_description: string;
manage_users?: boolean;
manage_apis?: boolean;
manage_endpoints?: boolean;
manage_groups?: boolean;
manage_roles?: boolean;
manage_routings?: boolean;
manage_gateway?: boolean;
manage_subscriptions?: boolean;
role_name: string
role_description: string
manage_users?: boolean
manage_apis?: boolean
manage_endpoints?: boolean
manage_groups?: boolean
manage_roles?: boolean
manage_routings?: boolean
manage_gateway?: boolean
manage_subscriptions?: boolean
}
const menuItems = [
{ label: 'Dashboard', href: '/dashboard' },
{ label: 'APIs', href: '/apis' },
{ label: 'Routings', href: '/routings' },
{ label: 'Users', href: '/users' },
{ label: 'Groups', href: '/groups' },
{ label: 'Roles', href: '/roles' },
{ label: 'Monitor', href: '/monitor' },
{ label: 'Logs', href: '/logging' },
{ label: 'Security', href: '/security' },
{ label: 'Settings', href: '/settings' },
];
const handleLogout = () => {
localStorage.clear();
sessionStorage.clear();
setTimeout(() => {
window.location.replace('/');
}, 50);
};
const RolesPage = () => {
const router = useRouter();
const [theme, setTheme] = useState('light');
const [roles, setRoles] = useState<Role[]>([]);
const [allRoles, setAllRoles] = useState<Role[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [searchTerm, setSearchTerm] = useState('');
const [sortBy, setSortBy] = useState('role_name');
const router = useRouter()
const [roles, setRoles] = useState<Role[]>([])
const [allRoles, setAllRoles] = useState<Role[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [searchTerm, setSearchTerm] = useState('')
const [sortBy, setSortBy] = useState('role_name')
useEffect(() => {
const savedTheme = localStorage.getItem('theme') || 'light';
setTheme(savedTheme);
document.documentElement.setAttribute('data-theme', savedTheme);
}, []);
useEffect(() => {
fetchRoles();
}, []);
fetchRoles()
}, [])
const fetchRoles = async () => {
try {
setLoading(true);
setError(null);
setLoading(true)
setError(null)
const response = await fetch(`http://localhost:3002/platform/role/all?page=1&page_size=10`, {
credentials: 'include',
headers: {
@@ -70,173 +42,219 @@ const RolesPage = () => {
'Content-Type': 'application/json',
'Cookie': `access_token_cookie=${document.cookie.split('; ').find(row => row.startsWith('access_token_cookie='))?.split('=')[1]}`
}
});
})
if (!response.ok) {
throw new Error('Failed to load roles');
throw new Error('Failed to load roles')
}
const data = await response.json();
const data = await response.json()
setAllRoles(data.roles);
setRoles(data.roles);
setAllRoles(data.roles)
setRoles(data.roles)
} catch (err) {
setError('Failed to load roles. Please try again later.');
setRoles([]);
setAllRoles([]);
setError('Failed to load roles. Please try again later.')
setRoles([])
setAllRoles([])
} finally {
setLoading(false);
setLoading(false)
}
};
}
const handleSearch = (e: React.FormEvent) => {
e.preventDefault();
e.preventDefault()
if (!searchTerm.trim()) {
setRoles(allRoles);
return;
setRoles(allRoles)
return
}
const filteredRoles = allRoles.filter(role =>
role.role_name.toLowerCase().includes(searchTerm.toLowerCase()) ||
role.role_description?.toLowerCase().includes(searchTerm.toLowerCase())
);
setRoles(filteredRoles);
};
)
setRoles(filteredRoles)
}
const handleSort = (sortField: string) => {
setSortBy(sortField);
setSortBy(sortField)
const sortedRoles = [...roles].sort((a, b) => {
if (sortField === 'role_name') {
return a.role_name.localeCompare(b.role_name);
return a.role_name.localeCompare(b.role_name)
} else if (sortField === 'permissions') {
const aPerms = Object.values(a).filter(val => typeof val === 'boolean' && val).length;
const bPerms = Object.values(b).filter(val => typeof val === 'boolean' && val).length;
return bPerms - aPerms;
const aPerms = Object.values(a).filter(val => typeof val === 'boolean' && val).length
const bPerms = Object.values(b).filter(val => typeof val === 'boolean' && val).length
return bPerms - aPerms
}
return 0;
});
setRoles(sortedRoles);
};
return 0
})
setRoles(sortedRoles)
}
const handleRoleClick = (role: Role) => {
// Store role data in sessionStorage for the detail page
sessionStorage.setItem('selectedRole', JSON.stringify(role));
router.push(`/roles/${role.role_name}`);
};
sessionStorage.setItem('selectedRole', JSON.stringify(role))
router.push(`/roles/${role.role_name}`)
}
const getPermissionCount = (role: Role) => {
return Object.values(role).filter(val => typeof val === 'boolean' && val).length;
};
return Object.values(role).filter(val => typeof val === 'boolean' && val).length
}
return (
<>
<div className="roles-topbar">
Doorman
</div>
<div className="roles-root">
<aside className="roles-sidebar">
<div className="roles-sidebar-title">Menu</div>
<ul className="roles-sidebar-list">
{menuItems.map((item, idx) => (
item.href ? (
<li key={item.label} className={`roles-sidebar-item${idx === 5 ? ' active' : ''}`}>
<Link href={item.href} style={{ color: 'inherit', textDecoration: 'none', display: 'block', width: '100%' }}>{item.label}</Link>
</li>
) : (
<li key={item.label} className={`roles-sidebar-item${idx === 5 ? ' active' : ''}`}>{item.label}</li>
)
))}
</ul>
<button className="roles-logout-btn" onClick={handleLogout}>
Logout
</button>
</aside>
<main className="roles-main">
<div className="roles-header-row">
<h1 className="roles-title">Roles</h1>
<button className="refresh-button" onClick={fetchRoles}>
<span className="refresh-icon"></span>
Refresh
</button>
<Layout>
<div className="space-y-6">
{/* Page Header */}
<div className="page-header">
<div>
<h1 className="page-title">Roles</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">
Manage user roles and permissions
</p>
</div>
<Link href="/roles/add" className="btn btn-primary">
<svg className="h-4 w-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
Add Role
</Link>
</div>
<div className="roles-controls">
<form className="roles-search-box" onSubmit={handleSearch}>
<input
type="text"
className="roles-search-input"
placeholder="Search roles..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
<button type="submit" className="roles-search-btn">Search</button>
<Link href="/roles/add" className="roles-add-btn" style={{ textDecoration: 'none', display: 'inline-block' }}>
Add Role
</Link>
{/* Search and Filters */}
<div className="card">
<div className="flex flex-col sm:flex-row gap-4">
<form onSubmit={handleSearch} className="flex-1">
<div className="relative">
<svg className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
<input
type="text"
className="search-input"
placeholder="Search roles by name or description..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
</form>
<div className="roles-sort-group">
<button
className={`roles-sort-btn ${sortBy === 'role_name' ? 'active' : ''}`}
<div className="flex gap-2">
<button
onClick={() => handleSort('role_name')}
className={`btn ${sortBy === 'role_name' ? 'btn-primary' : 'btn-secondary'}`}
>
Name
</button>
<button
className={`roles-sort-btn ${sortBy === 'permissions' ? 'active' : ''}`}
<button
onClick={() => handleSort('permissions')}
className={`btn ${sortBy === 'permissions' ? 'btn-primary' : 'btn-secondary'}`}
>
Permissions
</button>
</div>
</div>
</div>
{error && (
<div className="error-container">
<div className="error-message">
{error}
{/* Error Message */}
{error && (
<div className="rounded-lg bg-error-50 border border-error-200 p-4 dark:bg-error-900/20 dark:border-error-800">
<div className="flex">
<svg className="h-5 w-5 text-error-400 dark:text-error-500 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div className="ml-3">
<p className="text-sm text-error-700 dark:text-error-300">{error}</p>
</div>
</div>
)}
</div>
)}
{loading ? (
<div className="loading-spinner">
<div className="spinner"></div>
<p>Loading roles...</p>
{/* Loading State */}
{loading ? (
<div className="card">
<div className="flex items-center justify-center py-12">
<div className="text-center">
<div className="spinner mx-auto mb-4"></div>
<p className="text-gray-600 dark:text-gray-400">Loading roles...</p>
</div>
</div>
) : (
<div className="roles-table-panel">
<table className="roles-table">
</div>
) : (
/* Roles Table */
<div className="card">
<div className="overflow-x-auto">
<table className="table">
<thead>
<tr>
<th>Name</th>
<th>Description</th>
<th>Permissions</th>
<th></th>
</tr>
</thead>
<tbody>
{roles.map((role) => (
<tr
key={role.role_name}
key={role.role_name}
onClick={() => handleRoleClick(role)}
style={{ cursor: 'pointer' }}
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#f5f5f5'}
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = ''}
className="cursor-pointer hover:bg-gray-50 dark:hover:bg-dark-surfaceHover transition-colors"
>
<td><b>{role.role_name}</b></td>
<td>{role.role_description || 'No description'}</td>
<td>
<span className="roles-permissions-badge">
<div className="flex items-center">
<div className="h-8 w-8 rounded-lg bg-gradient-to-br from-purple-400 to-purple-600 flex items-center justify-center mr-3">
<svg className="h-4 w-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
</div>
<div>
<p className="font-medium text-gray-900 dark:text-white">{role.role_name}</p>
</div>
</div>
</td>
<td>
<p className="text-sm text-gray-600 dark:text-gray-400 max-w-xs truncate">
{role.role_description || 'No description'}
</p>
</td>
<td>
<span className="badge badge-warning">
{getPermissionCount(role)} permission{getPermissionCount(role) !== 1 ? 's' : ''}
</span>
</td>
<td>
<button className="btn btn-ghost btn-sm">
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</main>
</div>
</>
);
};
export default RolesPage;
{/* Empty State */}
{roles.length === 0 && !loading && (
<div className="text-center py-12">
<div className="h-16 w-16 mx-auto mb-4 rounded-full bg-gray-100 dark:bg-gray-800 flex items-center justify-center">
<svg className="h-8 w-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
</div>
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">No roles found</h3>
<p className="text-gray-600 dark:text-gray-400 mb-4">
{searchTerm ? 'Try adjusting your search terms.' : 'Get started by creating your first user role.'}
</p>
<Link href="/roles/add" className="btn btn-primary">
<svg className="h-4 w-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
Add Role
</Link>
</div>
)}
</div>
)}
</div>
</Layout>
)
}
export default RolesPage
+374 -321
View File
@@ -1,111 +1,89 @@
'use client'
import React, { useState, useEffect } from 'react';
import Link from 'next/link';
import { useRouter, useParams } from 'next/navigation';
import './routing-detail.css';
import React, { useState, useEffect } from 'react'
import Link from 'next/link'
import { useRouter, useParams } from 'next/navigation'
import Layout from '@/components/Layout'
interface Routing {
routing_name: string;
routing_servers: string[];
routing_description: string;
client_key: string;
server_index?: number;
routing_name: string
routing_servers: string[]
routing_description: string
client_key: string
server_index?: number
}
interface UpdateRoutingData {
routing_name?: string;
routing_servers?: string[];
routing_description?: string;
server_index?: number;
routing_name?: string
routing_servers?: string[]
routing_description?: string
server_index?: number
}
const menuItems = [
{ label: 'Dashboard', href: '/dashboard' },
{ label: 'APIs', href: '/apis' },
{ label: 'Routings', href: '/routings' },
{ label: 'Users', href: '/users' },
{ label: 'Groups', href: '/groups' },
{ label: 'Roles', href: '/roles' },
{ label: 'Monitor', href: '/monitor' },
{ label: 'Logs', href: '/logging' },
{ label: 'Security' },
{ label: 'Settings', href: '/settings' },
];
const handleLogout = () => {
localStorage.clear();
sessionStorage.clear();
setTimeout(() => {
window.location.replace('/');
}, 50);
};
const RoutingDetailPage = () => {
const router = useRouter();
const params = useParams();
const clientKey = params.clientKey as string;
const [routing, setRouting] = useState<Routing | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [isEditing, setIsEditing] = useState(false);
const [saving, setSaving] = useState(false);
const [editData, setEditData] = useState<UpdateRoutingData>({});
const [newServer, setNewServer] = useState('');
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [deleteConfirmation, setDeleteConfirmation] = useState('');
const [deleting, setDeleting] = useState(false);
const router = useRouter()
const params = useParams()
const clientKey = params.clientKey as string
const [routing, setRouting] = useState<Routing | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [success, setSuccess] = useState<string | null>(null)
const [isEditing, setIsEditing] = useState(false)
const [saving, setSaving] = useState(false)
const [editData, setEditData] = useState<UpdateRoutingData>({})
const [newServer, setNewServer] = useState('')
const [showDeleteModal, setShowDeleteModal] = useState(false)
const [deleteConfirmation, setDeleteConfirmation] = useState('')
const [deleting, setDeleting] = useState(false)
useEffect(() => {
const routingData = sessionStorage.getItem('selectedRouting');
const routingData = sessionStorage.getItem('selectedRouting')
if (routingData) {
try {
const parsedRouting = JSON.parse(routingData);
setRouting(parsedRouting);
const parsedRouting = JSON.parse(routingData)
setRouting(parsedRouting)
setEditData({
routing_name: parsedRouting.routing_name,
routing_servers: [...parsedRouting.routing_servers],
routing_description: parsedRouting.routing_description,
server_index: parsedRouting.server_index || 0
});
setLoading(false);
})
setLoading(false)
} catch (err) {
setError('Failed to load routing data');
setLoading(false);
setError('Failed to load routing data')
setLoading(false)
}
} else {
setError('No routing data found');
setLoading(false);
setError('No routing data found')
setLoading(false)
}
}, [clientKey]);
}, [clientKey])
const handleBack = () => {
router.back();
};
router.push('/routings')
}
const handleEdit = () => {
setIsEditing(true);
};
setIsEditing(true)
}
const handleCancel = () => {
setIsEditing(false);
setError(null);
// Reset edit data to original values
setIsEditing(false)
if (routing) {
setEditData({
routing_name: routing.routing_name,
routing_servers: [...routing.routing_servers],
routing_description: routing.routing_description,
server_index: routing.server_index || 0
});
})
}
};
}
const handleSave = async () => {
try {
setSaving(true);
setError(null);
setSaving(true)
setError(null)
const response = await fetch(`http://localhost:3002/platform/routing/${clientKey}`, {
method: 'PUT',
credentials: 'include',
@@ -115,76 +93,76 @@ const RoutingDetailPage = () => {
'Cookie': `access_token_cookie=${document.cookie.split('; ').find(row => row.startsWith('access_token_cookie='))?.split('=')[1]}`
},
body: JSON.stringify(editData)
});
})
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error_message || 'Failed to update routing');
const errorData = await response.json()
throw new Error(errorData.detail || 'Failed to update routing')
}
// Update the routing data in sessionStorage and state
if (routing) {
const updatedRouting: Routing = {
...routing,
...editData,
routing_name: editData.routing_name || routing.routing_name,
routing_servers: editData.routing_servers || routing.routing_servers,
routing_description: editData.routing_description || routing.routing_description,
server_index: editData.server_index ?? routing.server_index
};
sessionStorage.setItem('selectedRouting', JSON.stringify(updatedRouting));
setRouting(updatedRouting);
}
setIsEditing(false);
const updatedRouting = await response.json()
setRouting(updatedRouting)
setIsEditing(false)
setSuccess('Routing updated successfully!')
setTimeout(() => setSuccess(null), 3000)
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to update routing');
if (err instanceof Error) {
setError(err.message)
} else {
setError('Failed to update routing. Please try again.')
}
} finally {
setSaving(false);
setSaving(false)
}
};
}
const handleInputChange = (field: keyof UpdateRoutingData, value: any) => {
setEditData(prev => ({ ...prev, [field]: value }))
}
const handleServerChange = (index: number, value: string) => {
const newServers = [...(editData.routing_servers || [])];
newServers[index] = value;
setEditData(prev => ({ ...prev, routing_servers: newServers }));
};
setEditData(prev => ({
...prev,
routing_servers: prev.routing_servers?.map((server, i) => i === index ? value : server) || []
}))
}
const addServer = () => {
if (newServer.trim()) {
if (newServer.trim() && !editData.routing_servers?.includes(newServer.trim())) {
setEditData(prev => ({
...prev,
routing_servers: [...(prev.routing_servers || []), newServer.trim()]
}));
setNewServer('');
}))
setNewServer('')
}
};
}
const removeServer = (index: number) => {
setEditData(prev => ({
...prev,
routing_servers: (prev.routing_servers || []).filter((_, i) => i !== index)
}));
};
routing_servers: prev.routing_servers?.filter((_, i) => i !== index) || []
}))
}
const handleDeleteClick = () => {
setShowDeleteModal(true);
};
setShowDeleteModal(true)
}
const handleDeleteCancel = () => {
setShowDeleteModal(false);
setDeleteConfirmation('');
};
setShowDeleteModal(false)
setDeleteConfirmation('')
}
const handleDeleteConfirm = async () => {
if (deleteConfirmation !== routing?.routing_name) {
setError('Confirmation does not match routing name');
return;
setError('Routing name does not match')
return
}
try {
setDeleting(true);
setError(null);
setDeleting(true)
setError(null)
const response = await fetch(`http://localhost:3002/platform/routing/${clientKey}`, {
method: 'DELETE',
credentials: 'include',
@@ -193,254 +171,329 @@ const RoutingDetailPage = () => {
'Content-Type': 'application/json',
'Cookie': `access_token_cookie=${document.cookie.split('; ').find(row => row.startsWith('access_token_cookie='))?.split('=')[1]}`
}
});
})
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error_message || 'Failed to delete routing');
const errorData = await response.json()
throw new Error(errorData.detail || 'Failed to delete routing')
}
// Redirect to routings list after successful deletion
router.push('/routings');
router.push('/routings')
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to delete routing');
if (err instanceof Error) {
setError(err.message)
} else {
setError('Failed to delete routing. Please try again.')
}
} finally {
setDeleting(false);
setDeleting(false)
setShowDeleteModal(false)
}
};
}
if (loading) {
return (
<div className="loading-spinner">
<div className="spinner"></div>
<p>Loading routing...</p>
</div>
);
<Layout>
<div className="flex items-center justify-center py-12">
<div className="text-center">
<div className="spinner mx-auto mb-4"></div>
<p className="text-gray-600 dark:text-gray-400">Loading routing details...</p>
</div>
</div>
</Layout>
)
}
if (error && !routing) {
return (
<div className="error-container">
<div className="error-message">
{error}
<Layout>
<div className="space-y-6">
<div className="page-header">
<div>
<h1 className="page-title">Routing Details</h1>
</div>
<button onClick={handleBack} className="btn btn-secondary">
<svg className="h-4 w-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
Back to Routings
</button>
</div>
<div className="rounded-lg bg-error-50 border border-error-200 p-4 dark:bg-error-900/20 dark:border-error-800">
<div className="flex">
<svg className="h-5 w-5 text-error-400 dark:text-error-500 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div className="ml-3">
<p className="text-sm text-error-700 dark:text-error-300">{error}</p>
</div>
</div>
</div>
</div>
<button className="back-button" onClick={handleBack}>
<span className="back-arrow"></span>
Back to Routings
</button>
</div>
);
</Layout>
)
}
return (
<>
<div className="routings-topbar">
Doorman
</div>
<div className="routings-root">
<aside className="routings-sidebar">
<div className="routings-sidebar-title">Menu</div>
<ul className="routings-sidebar-list">
{menuItems.map((item, idx) => (
item.href ? (
<li key={item.label} className={`routings-sidebar-item${idx === 2 ? ' active' : ''}`}>
<Link href={item.href} style={{ color: 'inherit', textDecoration: 'none', display: 'block', width: '100%' }}>{item.label}</Link>
</li>
) : (
<li key={item.label} className={`routings-sidebar-item${idx === 2 ? ' active' : ''}`}>{item.label}</li>
)
))}
</ul>
<button className="routings-logout-btn" onClick={handleLogout}>
Logout
</button>
</aside>
<main className="routings-main">
<div className="routing-detail-header">
<button className="back-button" onClick={handleBack}>
<span className="back-arrow"></span>
Back to Routings
</button>
<h1 className="routing-detail-title">Routing Details</h1>
<Layout>
<div className="space-y-6">
{/* Page Header */}
<div className="page-header">
<div>
<h1 className="page-title">{routing?.routing_name || 'Routing Details'}</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">
Manage routing configuration and load balancing
</p>
</div>
<div className="flex gap-2">
{!isEditing ? (
<button className="edit-button" onClick={handleEdit}>
Edit Routing
</button>
) : (
<div className="edit-actions">
<button className="save-button" onClick={handleSave} disabled={saving}>
{saving ? 'Saving...' : 'Save Changes'}
<>
<button onClick={handleEdit} className="btn btn-primary">
<svg className="h-4 w-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
Edit Routing
</button>
<button className="cancel-button" onClick={handleCancel} disabled={saving}>
Cancel
</button>
<button className="delete-button" onClick={handleDeleteClick} disabled={saving}>
<button onClick={handleDeleteClick} className="btn btn-error">
<svg className="h-4 w-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
Delete Routing
</button>
</div>
</>
) : (
<>
<button onClick={handleSave} disabled={saving} className="btn btn-primary">
{saving ? (
<div className="flex items-center">
<div className="spinner mr-2"></div>
Saving...
</div>
) : (
<>
<svg className="h-4 w-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
Save Changes
</>
)}
</button>
<button onClick={handleCancel} className="btn btn-secondary">
Cancel
</button>
</>
)}
<button onClick={handleBack} className="btn btn-ghost">
<svg className="h-4 w-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
Back
</button>
</div>
</div>
{error && (
<div className="error-container">
<div className="error-message">
{error}
{/* Success Message */}
{success && (
<div className="rounded-lg bg-success-50 border border-success-200 p-4 dark:bg-success-900/20 dark:border-success-800">
<div className="flex">
<svg className="h-5 w-5 text-success-400 dark:text-success-500 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div className="ml-3">
<p className="text-sm text-success-700 dark:text-success-300">{success}</p>
</div>
</div>
)}
</div>
)}
{routing && (
<div className="routing-detail-content">
<div className="routing-detail-form">
<div className="form-section">
<h2 className="section-title">Basic Information</h2>
<div className="form-grid">
<div className="form-group">
<label className="form-label">Routing Name</label>
{/* Error Message */}
{error && (
<div className="rounded-lg bg-error-50 border border-error-200 p-4 dark:bg-error-900/20 dark:border-error-800">
<div className="flex">
<svg className="h-5 w-5 text-error-400 dark:text-error-500 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div className="ml-3">
<p className="text-sm text-error-700 dark:text-error-300">{error}</p>
</div>
</div>
</div>
)}
{routing && (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Basic Information */}
<div className="card">
<div className="card-header">
<h3 className="card-title">Basic Information</h3>
</div>
<div className="p-6 space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Routing Name
</label>
{isEditing ? (
<input
type="text"
value={editData.routing_name || ''}
onChange={(e) => handleInputChange('routing_name', e.target.value)}
className="input"
placeholder="Enter routing name"
/>
) : (
<p className="text-gray-900 dark:text-white">{routing.routing_name}</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Client Key
</label>
<code className="text-sm bg-gray-100 dark:bg-gray-800 px-2 py-1 rounded font-mono">
{routing.client_key}
</code>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Description
</label>
{isEditing ? (
<textarea
value={editData.routing_description || ''}
onChange={(e) => handleInputChange('routing_description', e.target.value)}
className="input resize-none"
rows={3}
placeholder="Enter routing description"
/>
) : (
<p className="text-gray-600 dark:text-gray-400">{routing.routing_description || 'No description'}</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Server Index
</label>
{isEditing ? (
<input
type="number"
value={editData.server_index || 0}
onChange={(e) => handleInputChange('server_index', parseInt(e.target.value))}
className="input"
min="0"
/>
) : (
<p className="text-gray-900 dark:text-white">{routing.server_index || 0}</p>
)}
</div>
</div>
</div>
{/* Servers Configuration */}
<div className="card">
<div className="card-header">
<h3 className="card-title">Servers Configuration</h3>
</div>
<div className="p-6 space-y-4">
{isEditing && (
<div className="flex gap-2">
<input
type="text"
value={newServer}
onChange={(e) => setNewServer(e.target.value)}
className="input flex-1"
placeholder="Enter server URL"
onKeyPress={(e) => e.key === 'Enter' && addServer()}
/>
<button onClick={addServer} className="btn btn-primary">
Add
</button>
</div>
)}
<div className="space-y-2">
{(isEditing ? editData.routing_servers : routing.routing_servers)?.map((server, index) => (
<div key={index} className="flex items-center gap-2">
{isEditing ? (
<input
type="text"
className="form-input"
value={editData.routing_name || ''}
onChange={(e) => setEditData(prev => ({ ...prev, routing_name: e.target.value }))}
maxLength={50}
value={server}
onChange={(e) => handleServerChange(index, e.target.value)}
className="input flex-1"
placeholder="Enter server URL"
/>
) : (
<div className="form-value">{routing.routing_name}</div>
<span className="text-sm font-mono text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-800 px-3 py-2 rounded flex-1">
{server}
</span>
)}
{isEditing && (
<button
onClick={() => removeServer(index)}
className="text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-200"
>
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
)}
</div>
<div className="form-group">
<label className="form-label">Client Key</label>
<div className="form-value">{routing.client_key}</div>
))}
</div>
{(!isEditing ? routing.routing_servers : editData.routing_servers)?.length === 0 && (
<p className="text-gray-500 dark:text-gray-400 text-sm">No servers configured</p>
)}
</div>
</div>
</div>
)}
{/* Delete Modal */}
{showDeleteModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center">
<div className="fixed inset-0 bg-black/50" onClick={handleDeleteCancel}></div>
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-md w-full mx-4 relative z-10">
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-4">Delete Routing</h3>
<p className="text-gray-600 dark:text-gray-400 mb-4">
This action cannot be undone. This will permanently delete the routing "{routing?.routing_name}".
</p>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-4">
Please type <strong>{routing?.routing_name}</strong> to confirm.
</p>
<input
type="text"
value={deleteConfirmation}
onChange={(e) => setDeleteConfirmation(e.target.value)}
className="input w-full mb-4"
placeholder="Enter routing name to confirm"
/>
<div className="flex gap-2">
<button
onClick={handleDeleteConfirm}
disabled={deleteConfirmation !== routing?.routing_name || deleting}
className="btn btn-error flex-1"
>
{deleting ? (
<div className="flex items-center justify-center">
<div className="spinner mr-2"></div>
Deleting...
</div>
</div>
<div className="form-group">
<label className="form-label">Description</label>
{isEditing ? (
<textarea
className="form-input"
value={editData.routing_description || ''}
onChange={(e) => setEditData(prev => ({ ...prev, routing_description: e.target.value }))}
maxLength={255}
rows={3}
/>
) : (
<div className="form-value">{routing.routing_description || 'No description'}</div>
)}
</div>
</div>
<div className="form-section">
<h2 className="section-title">Server Configuration</h2>
<div className="form-group">
<label className="form-label">Servers</label>
{isEditing ? (
<div className="servers-container">
<div className="servers-list">
{(editData.routing_servers || []).map((server, index) => (
<div key={index} className="server-input-group">
<input
type="text"
className="form-input"
value={server}
onChange={(e) => handleServerChange(index, e.target.value)}
placeholder="Server URL"
/>
<button
type="button"
className="remove-server-btn"
onClick={() => removeServer(index)}
>
×
</button>
</div>
))}
</div>
<div className="add-server">
<input
type="text"
className="form-input"
placeholder="Enter server URL"
value={newServer}
onChange={(e) => setNewServer(e.target.value)}
/>
<button
type="button"
className="add-button"
onClick={addServer}
>
Add Server
</button>
</div>
</div>
) : (
<div className="servers-list">
{routing.routing_servers.map((server, index) => (
<div key={index} className="server-tag">
<span>{server}</span>
</div>
))}
</div>
)}
</div>
<div className="form-group">
<label className="form-label">Default Server Index</label>
{isEditing ? (
<input
type="number"
className="form-input"
value={editData.server_index || 0}
onChange={(e) => setEditData(prev => ({ ...prev, server_index: parseInt(e.target.value) || 0 }))}
min={0}
/>
) : (
<div className="form-value">{routing.server_index || 0}</div>
)}
</div>
</div>
) : (
'Delete Routing'
)}
</button>
<button onClick={handleDeleteCancel} className="btn btn-secondary flex-1">
Cancel
</button>
</div>
</div>
)}
{/* Delete Confirmation Modal */}
{showDeleteModal && (
<div className="modal-overlay">
<div className="modal-content">
<h3>Delete Routing</h3>
<p>Are you sure you want to delete this routing? This action cannot be undone.</p>
<p>To confirm, please type the routing name: <strong>{routing?.routing_name}</strong></p>
<input
type="text"
className="form-input"
placeholder="Enter routing name to confirm"
value={deleteConfirmation}
onChange={(e) => setDeleteConfirmation(e.target.value)}
/>
<div className="modal-actions">
<button
type="button"
className="cancel-button"
onClick={handleDeleteCancel}
disabled={deleting}
>
Cancel
</button>
<button
type="button"
className="delete-button"
onClick={handleDeleteConfirm}
disabled={deleting || deleteConfirmation !== routing?.routing_name}
>
{deleting ? 'Deleting...' : 'Delete Routing'}
</button>
</div>
</div>
</div>
)}
</main>
</div>
)}
</div>
</>
);
};
</Layout>
)
}
export default RoutingDetailPage;
export default RoutingDetailPage
+186 -203
View File
@@ -1,79 +1,57 @@
'use client'
import React, { useState } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import '../routings.css';
import './add-routing.css';
import React, { useState } from 'react'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
import Layout from '@/components/Layout'
interface CreateRoutingData {
routing_name: string;
routing_servers: string[];
routing_description: string;
client_key?: string;
server_index?: number;
routing_name: string
routing_servers: string[]
routing_description: string
client_key?: string
server_index?: number
}
const menuItems = [
{ label: 'Dashboard', href: '/dashboard' },
{ label: 'APIs', href: '/apis' },
{ label: 'Routings', href: '/routings' },
{ label: 'Users', href: '/users' },
{ label: 'Groups', href: '/groups' },
{ label: 'Roles', href: '/roles' },
{ label: 'Monitor', href: '/monitor' },
{ label: 'Logs', href: '/logging' },
{ label: 'Security' },
{ label: 'Settings', href: '/settings' },
];
const handleLogout = () => {
localStorage.clear();
sessionStorage.clear();
setTimeout(() => {
window.location.replace('/');
}, 50);
};
const AddRoutingPage = () => {
const router = useRouter();
const router = useRouter()
const [formData, setFormData] = useState<CreateRoutingData>({
routing_name: '',
routing_servers: [],
routing_description: '',
server_index: 0
});
const [newServer, setNewServer] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
})
const [newServer, setNewServer] = useState('')
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const handleInputChange = (field: keyof CreateRoutingData, value: any) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
setFormData(prev => ({ ...prev, [field]: value }))
}
const addServer = () => {
if (newServer.trim()) {
setFormData(prev => ({ ...prev, routing_servers: [...prev.routing_servers, newServer.trim()] }));
setNewServer('');
if (newServer.trim() && !formData.routing_servers.includes(newServer.trim())) {
setFormData(prev => ({ ...prev, routing_servers: [...prev.routing_servers, newServer.trim()] }))
setNewServer('')
}
};
}
const removeServer = (index: number) => {
setFormData(prev => ({ ...prev, routing_servers: prev.routing_servers.filter((_, i) => i !== index) }));
};
setFormData(prev => ({ ...prev, routing_servers: prev.routing_servers.filter((_, i) => i !== index) }))
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
e.preventDefault()
// Validate required fields
if (!formData.routing_name || formData.routing_servers.length === 0) {
setError('Please fill in routing name and add at least one server');
return;
setError('Please fill in routing name and add at least one server')
return
}
try {
setLoading(true);
setError(null);
setLoading(true)
setError(null)
const response = await fetch('http://localhost:3002/platform/routing/', {
method: 'POST',
@@ -84,179 +62,184 @@ const AddRoutingPage = () => {
'Cookie': `access_token_cookie=${document.cookie.split('; ').find(row => row.startsWith('access_token_cookie='))?.split('=')[1]}`
},
body: JSON.stringify(formData)
});
})
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error_message || 'Failed to create routing');
const errorData = await response.json()
throw new Error(errorData.detail || 'Failed to create routing')
}
// Redirect to routings list after successful creation
router.push('/routings');
router.push('/routings')
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to create routing');
if (err instanceof Error) {
setError(err.message)
} else {
setError('Failed to create routing. Please try again.')
}
} finally {
setLoading(false);
setLoading(false)
}
};
const handleBack = () => {
router.back();
};
}
return (
<>
<div className="routings-topbar">
Doorman
</div>
<div className="routings-root">
<aside className="routings-sidebar">
<div className="routings-sidebar-title">Menu</div>
<ul className="routings-sidebar-list">
{menuItems.map((item, idx) => (
item.href ? (
<li key={item.label} className={`routings-sidebar-item${idx === 2 ? ' active' : ''}`}>
<Link href={item.href} style={{ color: 'inherit', textDecoration: 'none', display: 'block', width: '100%' }}>{item.label}</Link>
</li>
) : (
<li key={item.label} className={`routings-sidebar-item${idx === 2 ? ' active' : ''}`}>{item.label}</li>
)
))}
</ul>
<button className="routings-logout-btn" onClick={handleLogout}>
Logout
</button>
</aside>
<main className="routings-main">
<div className="routings-header-row">
<div className="add-routing-header">
<button className="back-button" onClick={handleBack}>
<span className="back-arrow"></span>
Back to Routings
</button>
<h1 className="routings-title">Add New Routing</h1>
<Layout>
<div className="space-y-6">
{/* Page Header */}
<div className="page-header">
<div>
<h1 className="page-title">Add Routing</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">
Create a new routing configuration for load balancing
</p>
</div>
</div>
{/* Error Message */}
{error && (
<div className="rounded-lg bg-error-50 border border-error-200 p-4 dark:bg-error-900/20 dark:border-error-800">
<div className="flex">
<svg className="h-5 w-5 text-error-400 dark:text-error-500 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div className="ml-3">
<p className="text-sm text-error-700 dark:text-error-300">{error}</p>
</div>
</div>
</div>
)}
{error && (
<div className="error-container">
<div className="error-message">
{error}
</div>
{/* Form */}
<div className="card max-w-2xl">
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label htmlFor="routing_name" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Routing Name *
</label>
<input
type="text"
id="routing_name"
name="routing_name"
className="input"
placeholder="Enter routing name"
value={formData.routing_name}
onChange={(e) => handleInputChange('routing_name', e.target.value)}
disabled={loading}
required
/>
</div>
)}
<div className="add-routing-content">
<form className="add-routing-form" onSubmit={handleSubmit}>
<div className="form-section">
<h2 className="section-title">Basic Information</h2>
<div className="form-grid">
<div className="form-group">
<label className="form-label">Routing Name *</label>
<input
type="text"
className="form-input"
placeholder="Enter routing name"
value={formData.routing_name}
onChange={(e) => handleInputChange('routing_name', e.target.value)}
maxLength={50}
/>
</div>
<div className="form-group">
<label className="form-label">Client Key (Optional)</label>
<input
type="text"
className="form-input"
placeholder="Enter client key"
value={formData.client_key || ''}
onChange={(e) => handleInputChange('client_key', e.target.value)}
maxLength={50}
/>
</div>
</div>
<div className="form-group">
<label className="form-label">Description</label>
<textarea
className="form-input"
placeholder="Enter routing description"
value={formData.routing_description}
onChange={(e) => handleInputChange('routing_description', e.target.value)}
maxLength={255}
rows={3}
/>
</div>
</div>
<div>
<label htmlFor="routing_description" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Description
</label>
<textarea
id="routing_description"
name="routing_description"
rows={4}
className="input resize-none"
placeholder="Describe the purpose of this routing..."
value={formData.routing_description}
onChange={(e) => handleInputChange('routing_description', e.target.value)}
disabled={loading}
/>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
Optional description of the routing configuration
</p>
</div>
<div className="form-section">
<h2 className="section-title">Server Configuration</h2>
<div className="servers-container">
<div className="servers-list">
{formData.routing_servers.map((server, index) => (
<div key={index} className="server-tag">
<span>{server}</span>
<button
type="button"
className="remove-server-btn"
onClick={() => removeServer(index)}
>
×
</button>
</div>
))}
</div>
<div className="add-server">
<input
type="text"
className="form-input"
placeholder="Enter server URL (e.g., http://localhost:8080)"
value={newServer}
onChange={(e) => setNewServer(e.target.value)}
/>
<button
type="button"
className="add-button"
onClick={addServer}
>
Add Server
</button>
</div>
</div>
<div className="form-group">
<label className="form-label">Default Server Index</label>
<div>
<label htmlFor="server_index" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Default Server Index
</label>
<input
type="number"
id="server_index"
name="server_index"
className="input"
placeholder="0"
value={formData.server_index || 0}
onChange={(e) => handleInputChange('server_index', parseInt(e.target.value))}
disabled={loading}
min="0"
/>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
Index of the default server in the list (0-based)
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Servers *
</label>
<div className="space-y-3">
<div className="flex gap-2">
<input
type="number"
className="form-input"
placeholder="0"
value={formData.server_index || 0}
onChange={(e) => handleInputChange('server_index', parseInt(e.target.value) || 0)}
min={0}
type="text"
className="input flex-1"
placeholder="Enter server URL (e.g., http://localhost:8080)"
value={newServer}
onChange={(e) => setNewServer(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && (e.preventDefault(), addServer())}
disabled={loading}
/>
<button
type="button"
onClick={addServer}
disabled={loading || !newServer.trim()}
className="btn btn-primary"
>
Add
</button>
</div>
<div className="space-y-2">
{formData.routing_servers.map((server, index) => (
<div key={index} className="flex items-center justify-between bg-gray-100 dark:bg-gray-800 px-3 py-2 rounded">
<span className="text-sm font-mono text-gray-700 dark:text-gray-300">{server}</span>
<button
type="button"
onClick={() => removeServer(index)}
className="text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-200"
disabled={loading}
>
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
))}
</div>
{formData.routing_servers.length === 0 && (
<p className="text-gray-500 dark:text-gray-400 text-sm">No servers added yet</p>
)}
</div>
</div>
<div className="form-actions">
<button
type="button"
className="cancel-button"
onClick={handleBack}
disabled={loading}
>
Cancel
</button>
<button
type="submit"
className="save-button"
disabled={loading}
>
{loading ? 'Creating...' : 'Create Routing'}
</button>
</div>
</form>
</div>
</main>
<div className="flex gap-4 pt-6 border-t border-gray-200 dark:border-gray-700">
<button
type="submit"
disabled={loading}
className="btn btn-primary flex-1"
>
{loading ? (
<div className="flex items-center justify-center">
<div className="spinner mr-2"></div>
Creating Routing...
</div>
) : (
'Create Routing'
)}
</button>
<Link href="/routings" className="btn btn-secondary flex-1">
Cancel
</Link>
</div>
</form>
</div>
</div>
</>
);
};
</Layout>
)
}
export default AddRoutingPage;
export default AddRoutingPage
+174 -152
View File
@@ -1,62 +1,34 @@
'use client'
import React, { useState, useEffect } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import './routings.css';
import React, { useState, useEffect } from 'react'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
import Layout from '@/components/Layout'
interface Routing {
routing_name: string;
routing_servers: string[];
routing_description: string;
client_key: string;
routing_name: string
routing_servers: string[]
routing_description: string
client_key: string
}
const menuItems = [
{ label: 'Dashboard', href: '/dashboard' },
{ label: 'APIs', href: '/apis' },
{ label: 'Routings', href: '/routings' },
{ label: 'Users', href: '/users' },
{ label: 'Groups', href: '/groups' },
{ label: 'Roles', href: '/roles' },
{ label: 'Monitor', href: '/monitor' },
{ label: 'Logs', href: '/logging' },
{ label: 'Security' },
{ label: 'Settings', href: '/settings' },
];
const handleLogout = () => {
localStorage.clear();
sessionStorage.clear();
setTimeout(() => {
window.location.replace('/');
}, 50);
};
const RoutingsPage = () => {
const router = useRouter();
const [theme, setTheme] = useState('light');
const [routings, setRoutings] = useState<Routing[]>([]);
const [allRoutings, setAllRoutings] = useState<Routing[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [searchTerm, setSearchTerm] = useState('');
const [sortBy, setSortBy] = useState('routing_name');
const router = useRouter()
const [routings, setRoutings] = useState<Routing[]>([])
const [allRoutings, setAllRoutings] = useState<Routing[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [searchTerm, setSearchTerm] = useState('')
const [sortBy, setSortBy] = useState('routing_name')
useEffect(() => {
const savedTheme = localStorage.getItem('theme') || 'light';
setTheme(savedTheme);
document.documentElement.setAttribute('data-theme', savedTheme);
}, []);
useEffect(() => {
fetchRoutings();
}, []);
fetchRoutings()
}, [])
const fetchRoutings = async () => {
try {
setLoading(true);
setError(null);
setLoading(true)
setError(null)
const response = await fetch(`http://localhost:3002/platform/routing/all?page=1&page_size=10`, {
credentials: 'include',
headers: {
@@ -64,28 +36,28 @@ const RoutingsPage = () => {
'Content-Type': 'application/json',
'Cookie': `access_token_cookie=${document.cookie.split('; ').find(row => row.startsWith('access_token_cookie='))?.split('=')[1]}`
}
});
})
if (!response.ok) {
throw new Error('Failed to load routings');
throw new Error('Failed to load routings')
}
const data = await response.json();
const routingList = Array.isArray(data) ? data : (data.routings || data.response?.routings || []);
setAllRoutings(routingList);
setRoutings(routingList);
const data = await response.json()
const routingList = Array.isArray(data) ? data : (data.routings || data.response?.routings || [])
setAllRoutings(routingList)
setRoutings(routingList)
} catch (err) {
setError('Failed to load routings. Please try again later.');
setRoutings([]);
setAllRoutings([]);
setError('Failed to load routings. Please try again later.')
setRoutings([])
setAllRoutings([])
} finally {
setLoading(false);
setLoading(false)
}
};
}
const handleSearch = (e: React.FormEvent) => {
e.preventDefault();
e.preventDefault()
if (!searchTerm.trim()) {
setRoutings(allRoutings);
return;
setRoutings(allRoutings)
return
}
const filteredRoutings = allRoutings.filter(routing =>
@@ -93,150 +65,200 @@ const RoutingsPage = () => {
routing.routing_description?.toLowerCase().includes(searchTerm.toLowerCase()) ||
routing.client_key.toLowerCase().includes(searchTerm.toLowerCase()) ||
routing.routing_servers.some(server => server.toLowerCase().includes(searchTerm.toLowerCase()))
);
setRoutings(filteredRoutings);
};
)
setRoutings(filteredRoutings)
}
const handleSort = (sortField: string) => {
setSortBy(sortField);
setSortBy(sortField)
const sortedRoutings = [...routings].sort((a, b) => {
if (sortField === 'routing_name') {
return a.routing_name.localeCompare(b.routing_name);
return a.routing_name.localeCompare(b.routing_name)
} else if (sortField === 'client_key') {
return a.client_key.localeCompare(b.client_key);
return a.client_key.localeCompare(b.client_key)
} else if (sortField === 'servers') {
return a.routing_servers.length - b.routing_servers.length;
return a.routing_servers.length - b.routing_servers.length
}
return 0;
});
setRoutings(sortedRoutings);
};
return 0
})
setRoutings(sortedRoutings)
}
const handleRoutingClick = (routing: Routing) => {
// Store routing data in sessionStorage for the detail page
sessionStorage.setItem('selectedRouting', JSON.stringify(routing));
router.push(`/routings/${routing.client_key}`);
};
sessionStorage.setItem('selectedRouting', JSON.stringify(routing))
router.push(`/routings/${routing.client_key}`)
}
return (
<>
<div className="routings-topbar">
Doorman
</div>
<div className="routings-root">
<aside className="routings-sidebar">
<div className="routings-sidebar-title">Menu</div>
<ul className="routings-sidebar-list">
{menuItems.map((item, idx) => (
item.href ? (
<li key={item.label} className={`routings-sidebar-item${idx === 2 ? ' active' : ''}`}>
<Link href={item.href} style={{ color: 'inherit', textDecoration: 'none', display: 'block', width: '100%' }}>{item.label}</Link>
</li>
) : (
<li key={item.label} className={`routings-sidebar-item${idx === 2 ? ' active' : ''}`}>{item.label}</li>
)
))}
</ul>
<button className="routings-logout-btn" onClick={handleLogout}>
Logout
</button>
</aside>
<main className="routings-main">
<div className="routings-header-row">
<h1 className="routings-title">Routings</h1>
<button className="refresh-button" onClick={fetchRoutings}>
<span className="refresh-icon"></span>
Refresh
</button>
<Layout>
<div className="space-y-6">
{/* Page Header */}
<div className="page-header">
<div>
<h1 className="page-title">Routings</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">
Manage API routing configurations and load balancing
</p>
</div>
<Link href="/routings/add" className="btn btn-primary">
<svg className="h-4 w-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
Add Routing
</Link>
</div>
<div className="routings-controls">
<form className="routings-search-box" onSubmit={handleSearch}>
<input
type="text"
className="routings-search-input"
placeholder="Search routings..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
<button type="submit" className="routings-search-btn">Search</button>
<Link href="/routings/add" className="routings-add-btn" style={{ textDecoration: 'none', display: 'inline-block' }}>
Add Routing
</Link>
{/* Search and Filters */}
<div className="card">
<div className="flex flex-col sm:flex-row gap-4">
<form onSubmit={handleSearch} className="flex-1">
<div className="relative">
<svg className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
<input
type="text"
className="search-input"
placeholder="Search routings by name, description, or servers..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
</form>
<div className="routings-sort-group">
<button
className={`routings-sort-btn ${sortBy === 'routing_name' ? 'active' : ''}`}
<div className="flex gap-2">
<button
onClick={() => handleSort('routing_name')}
className={`btn ${sortBy === 'routing_name' ? 'btn-primary' : 'btn-secondary'}`}
>
Name
</button>
<button
className={`routings-sort-btn ${sortBy === 'client_key' ? 'active' : ''}`}
<button
onClick={() => handleSort('client_key')}
className={`btn ${sortBy === 'client_key' ? 'btn-primary' : 'btn-secondary'}`}
>
Key
</button>
<button
className={`routings-sort-btn ${sortBy === 'servers' ? 'active' : ''}`}
<button
onClick={() => handleSort('servers')}
className={`btn ${sortBy === 'servers' ? 'btn-primary' : 'btn-secondary'}`}
>
Servers
</button>
</div>
</div>
</div>
{error && (
<div className="error-container">
<div className="error-message">
{error}
{/* Error Message */}
{error && (
<div className="rounded-lg bg-error-50 border border-error-200 p-4 dark:bg-error-900/20 dark:border-error-800">
<div className="flex">
<svg className="h-5 w-5 text-error-400 dark:text-error-500 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div className="ml-3">
<p className="text-sm text-error-700 dark:text-error-300">{error}</p>
</div>
</div>
)}
</div>
)}
{loading ? (
<div className="loading-spinner">
<div className="spinner"></div>
<p>Loading routings...</p>
{/* Loading State */}
{loading ? (
<div className="card">
<div className="flex items-center justify-center py-12">
<div className="text-center">
<div className="spinner mx-auto mb-4"></div>
<p className="text-gray-600 dark:text-gray-400">Loading routings...</p>
</div>
</div>
) : (
<div className="routings-table-panel">
<table className="routings-table">
</div>
) : (
/* Routings Table */
<div className="card">
<div className="overflow-x-auto">
<table className="table">
<thead>
<tr>
<th>Name</th>
<th>Client Key</th>
<th>Description</th>
<th>Servers</th>
<th></th>
</tr>
</thead>
<tbody>
{routings.map((routing) => (
<tr
key={routing.client_key}
key={routing.client_key}
onClick={() => handleRoutingClick(routing)}
style={{ cursor: 'pointer' }}
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#f5f5f5'}
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = ''}
className="cursor-pointer hover:bg-gray-50 dark:hover:bg-dark-surfaceHover transition-colors"
>
<td>{routing.routing_name}</td>
<td>{routing.client_key}</td>
<td>{routing.routing_description || 'No description'}</td>
<td>
<span className="routings-servers-badge">
<div className="flex items-center">
<div className="h-8 w-8 rounded-lg bg-gradient-to-br from-blue-400 to-blue-600 flex items-center justify-center mr-3">
<svg className="h-4 w-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01m-7.08-7.071c3.904-3.905 10.236-3.905 14.141 0M1.394 9.393c5.857-5.857 15.355-5.857 21.213 0" />
</svg>
</div>
<div>
<p className="font-medium text-gray-900 dark:text-white">{routing.routing_name}</p>
</div>
</div>
</td>
<td>
<code className="text-sm bg-gray-100 dark:bg-gray-800 px-2 py-1 rounded font-mono">
{routing.client_key}
</code>
</td>
<td>
<p className="text-sm text-gray-600 dark:text-gray-400 max-w-xs truncate">
{routing.routing_description || 'No description'}
</p>
</td>
<td>
<span className="badge badge-primary">
{routing.routing_servers.length} server{routing.routing_servers.length !== 1 ? 's' : ''}
</span>
</td>
<td>
<button className="btn btn-ghost btn-sm">
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</main>
</div>
</>
);
};
export default RoutingsPage;
{/* Empty State */}
{routings.length === 0 && !loading && (
<div className="text-center py-12">
<div className="h-16 w-16 mx-auto mb-4 rounded-full bg-gray-100 dark:bg-gray-800 flex items-center justify-center">
<svg className="h-8 w-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01m-7.08-7.071c3.904-3.905 10.236-3.905 14.141 0M1.394 9.393c5.857-5.857 15.355-5.857 21.213 0" />
</svg>
</div>
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">No routings found</h3>
<p className="text-gray-600 dark:text-gray-400 mb-4">
{searchTerm ? 'Try adjusting your search terms.' : 'Get started by creating your first routing configuration.'}
</p>
<Link href="/routings/add" className="btn btn-primary">
<svg className="h-4 w-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
Add Routing
</Link>
</div>
)}
</div>
)}
</div>
</Layout>
)
}
export default RoutingsPage
+283 -322
View File
@@ -1,338 +1,231 @@
'use client'
import React, { useState, useEffect } from 'react';
import Link from 'next/link';
import './security.css';
import React, { useState, useEffect } from 'react'
import Layout from '@/components/Layout'
interface ApiKey {
id: string;
name: string;
key: string;
created: string;
lastUsed: string;
status: 'active' | 'revoked';
id: string
name: string
key: string
created: string
lastUsed: string
status: 'active' | 'revoked'
}
interface RateLimit {
id: string;
path: string;
limit: number;
window: string;
status: 'active' | 'disabled';
id: string
path: string
limit: number
window: string
status: 'active' | 'disabled'
}
interface IpWhitelist {
id: string;
ip: string;
description: string;
created: string;
status: 'active' | 'disabled';
id: string
ip: string
description: string
created: string
status: 'active' | 'disabled'
}
interface SecurityPolicy {
id: string;
name: string;
type: 'jwt' | 'oauth2' | 'api-key' | 'ip-whitelist';
status: 'active' | 'disabled';
createdAt: string;
id: string
name: string
type: 'jwt' | 'oauth2' | 'api-key' | 'ip-whitelist'
status: 'active' | 'disabled'
createdAt: string
}
const menuItems = [
{ label: 'Dashboard', href: '/dashboard' },
{ label: 'APIs', href: '/apis' },
{ label: 'Routings', href: '/routings' },
{ label: 'Users', href: '/users' },
{ label: 'Groups', href: '/groups' },
{ label: 'Roles', href: '/roles' },
{ label: 'Monitor', href: '/monitor' },
{ label: 'Logs', href: '/logging' },
{ label: 'Security', href: '/security' },
{ label: 'Settings', href: '/settings' },
];
const handleLogout = () => {
localStorage.clear();
sessionStorage.clear();
setTimeout(() => {
window.location.replace('/');
}, 50);
};
const SecurityPage = () => {
const [theme, setTheme] = useState('light');
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null);
const [activeTab, setActiveTab] = useState('api-keys');
const [apiKeys, setApiKeys] = useState<ApiKey[]>([]);
const [rateLimits, setRateLimits] = useState<RateLimit[]>([]);
const [ipWhitelist, setIpWhitelist] = useState<IpWhitelist[]>([]);
const [securityPolicies, setSecurityPolicies] = useState<SecurityPolicy[]>([]);
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [success, setSuccess] = useState<string | null>(null)
const [activeTab, setActiveTab] = useState('api-keys')
const [apiKeys, setApiKeys] = useState<ApiKey[]>([])
const [rateLimits, setRateLimits] = useState<RateLimit[]>([])
const [ipWhitelist, setIpWhitelist] = useState<IpWhitelist[]>([])
const [securityPolicies, setSecurityPolicies] = useState<SecurityPolicy[]>([])
useEffect(() => {
const savedTheme = localStorage.getItem('theme') || 'light';
setTheme(savedTheme);
document.documentElement.setAttribute('data-theme', savedTheme);
fetchSecurityData();
}, []);
fetchSecurityData()
}, [])
const fetchSecurityData = async () => {
try {
setLoading(true);
setError(null);
const [keysRes, limitsRes, whitelistRes, policiesRes] = await Promise.all([
fetch('/api/security/keys'),
fetch('/api/security/rate-limits'),
fetch('/api/security/ip-whitelist'),
fetch('/api/security/policies')
]);
if (!keysRes.ok || !limitsRes.ok || !whitelistRes.ok || !policiesRes.ok) {
throw new Error('Failed to load security data');
}
const [keys, limits, whitelist, policies] = await Promise.all([
keysRes.json(),
limitsRes.json(),
whitelistRes.json(),
policiesRes.json()
]);
setApiKeys(keys);
setRateLimits(limits);
setIpWhitelist(whitelist);
setSecurityPolicies(policies);
setLoading(true)
setError(null)
// Mock data for demonstration
setApiKeys([
{ id: '1', name: 'Production API Key', key: 'sk_prod_123456789', created: '2024-01-15', lastUsed: '2024-01-20', status: 'active' },
{ id: '2', name: 'Development API Key', key: 'sk_dev_987654321', created: '2024-01-10', lastUsed: '2024-01-19', status: 'active' }
])
setRateLimits([
{ id: '1', path: '/api/v1/*', limit: 1000, window: '1 hour', status: 'active' },
{ id: '2', path: '/api/v1/auth/*', limit: 100, window: '1 hour', status: 'active' }
])
setIpWhitelist([
{ id: '1', ip: '192.168.1.100', description: 'Office Network', created: '2024-01-15', status: 'active' },
{ id: '2', ip: '10.0.0.50', description: 'VPN Server', created: '2024-01-10', status: 'active' }
])
setSecurityPolicies([
{ id: '1', name: 'JWT Authentication', type: 'jwt', status: 'active', createdAt: '2024-01-15' },
{ id: '2', name: 'API Key Authentication', type: 'api-key', status: 'active', createdAt: '2024-01-10' }
])
} catch (err) {
setError('Failed to load security data. Please try again later.');
setError('Failed to load security data. Please try again later.')
} finally {
setLoading(false);
setLoading(false)
}
};
}
const handleCreateApiKey = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch('/api/security/keys', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ name: 'New API Key' }),
});
if (!response.ok) {
throw new Error('Failed to create API key');
}
const newKey = await response.json();
setApiKeys(prev => [...prev, newKey]);
setSuccess('API key created successfully');
setSuccess('API key created successfully!')
setTimeout(() => setSuccess(null), 3000)
} catch (err) {
setError('Failed to create API key. Please try again.');
} finally {
setLoading(false);
setError('Failed to create API key. Please try again.')
}
};
}
const handleRevokeApiKey = async (keyId: string) => {
try {
setLoading(true);
setError(null);
const response = await fetch(`/api/security/keys/${keyId}`, {
method: 'DELETE',
});
if (!response.ok) {
throw new Error('Failed to revoke API key');
}
setApiKeys(prev => prev.map(key =>
key.id === keyId ? { ...key, status: 'revoked' } : key
));
setSuccess('API key revoked successfully');
key.id === keyId ? { ...key, status: 'revoked' as const } : key
))
setSuccess('API key revoked successfully!')
setTimeout(() => setSuccess(null), 3000)
} catch (err) {
setError('Failed to revoke API key. Please try again.');
} finally {
setLoading(false);
setError('Failed to revoke API key. Please try again.')
}
};
}
const handleAddRateLimit = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch('/api/security/rate-limits', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
path: '/api/*',
limit: 100,
window: '1m',
}),
});
if (!response.ok) {
throw new Error('Failed to add rate limit');
}
const newLimit = await response.json();
setRateLimits(prev => [...prev, newLimit]);
setSuccess('Rate limit added successfully');
setSuccess('Rate limit added successfully!')
setTimeout(() => setSuccess(null), 3000)
} catch (err) {
setError('Failed to add rate limit. Please try again.');
} finally {
setLoading(false);
setError('Failed to add rate limit. Please try again.')
}
};
}
const handleAddIpWhitelist = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch('/api/security/ip-whitelist', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
ip: '192.168.1.1',
description: 'New IP Address',
}),
});
if (!response.ok) {
throw new Error('Failed to add IP to whitelist');
}
const newIp = await response.json();
setIpWhitelist(prev => [...prev, newIp]);
setSuccess('IP added to whitelist successfully');
setSuccess('IP address added to whitelist successfully!')
setTimeout(() => setSuccess(null), 3000)
} catch (err) {
setError('Failed to add IP to whitelist. Please try again.');
} finally {
setLoading(false);
setError('Failed to add IP to whitelist. Please try again.')
}
};
}
const handleCreateSecurityPolicy = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch('/api/security/policies', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: 'New Security Policy',
type: 'api-key',
}),
});
if (!response.ok) {
throw new Error('Failed to create security policy');
}
const newPolicy = await response.json();
setSecurityPolicies(prev => [...prev, newPolicy]);
setSuccess('Security policy created successfully');
setSuccess('Security policy created successfully!')
setTimeout(() => setSuccess(null), 3000)
} catch (err) {
setError('Failed to create security policy. Please try again.');
} finally {
setLoading(false);
setError('Failed to create security policy. Please try again.')
}
};
}
const tabs = [
{ id: 'api-keys', label: 'API Keys', icon: '🔑' },
{ id: 'rate-limits', label: 'Rate Limits', icon: '⏱️' },
{ id: 'ip-whitelist', label: 'IP Whitelist', icon: '🌐' },
{ id: 'policies', label: 'Security Policies', icon: '🛡️' }
]
return (
<>
<div className="security-topbar">
Doorman
</div>
<div className="security-root">
<aside className="security-sidebar">
<div className="security-sidebar-title">Menu</div>
<ul className="security-sidebar-list">
{menuItems.map((item, idx) => (
item.href ? (
<li key={item.label} className={`security-sidebar-item${idx === 8 ? ' active' : ''}`}>
<Link href={item.href} style={{ color: 'inherit', textDecoration: 'none', display: 'block', width: '100%' }}>{item.label}</Link>
</li>
) : (
<li key={item.label} className={`security-sidebar-item${idx === 8 ? ' active' : ''}`}>{item.label}</li>
)
))}
</ul>
<button className="security-logout-btn" onClick={handleLogout}>
Logout
</button>
</aside>
<main className="security-main">
<div className="security-header-row">
<h1 className="security-title">Security</h1>
<button className="refresh-button" onClick={fetchSecurityData}>
<span className="refresh-icon"></span>
Refresh
</button>
<Layout>
<div className="space-y-6">
{/* Page Header */}
<div className="page-header">
<div>
<h1 className="page-title">Security</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">
Manage API keys, rate limits, and security policies
</p>
</div>
</div>
{error && (
<div className="error-container">
<div className="error-message">
{error}
{/* Success Message */}
{success && (
<div className="rounded-lg bg-success-50 border border-success-200 p-4 dark:bg-success-900/20 dark:border-success-800">
<div className="flex">
<svg className="h-5 w-5 text-success-400 dark:text-success-500 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div className="ml-3">
<p className="text-sm text-success-700 dark:text-success-300">{success}</p>
</div>
</div>
)}
</div>
)}
{success && (
<div className="success-container">
<div className="success-message">
{success}
{/* Error Message */}
{error && (
<div className="rounded-lg bg-error-50 border border-error-200 p-4 dark:bg-error-900/20 dark:border-error-800">
<div className="flex">
<svg className="h-5 w-5 text-error-400 dark:text-error-500 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div className="ml-3">
<p className="text-sm text-error-700 dark:text-error-300">{error}</p>
</div>
</div>
)}
</div>
)}
{loading ? (
<div className="loading-spinner">
<div className="spinner"></div>
<p>Loading security data...</p>
{/* Loading State */}
{loading ? (
<div className="card">
<div className="flex items-center justify-center py-12">
<div className="text-center">
<div className="spinner mx-auto mb-4"></div>
<p className="text-gray-600 dark:text-gray-400">Loading security data...</p>
</div>
</div>
) : (
<div className="security-content">
<div className="security-tabs">
<button
className={`security-tab ${activeTab === 'api-keys' ? 'active' : ''}`}
onClick={() => setActiveTab('api-keys')}
>
API Keys
</button>
<button
className={`security-tab ${activeTab === 'rate-limits' ? 'active' : ''}`}
onClick={() => setActiveTab('rate-limits')}
>
Gateway Rate Limits
</button>
<button
className={`security-tab ${activeTab === 'ip-whitelist' ? 'active' : ''}`}
onClick={() => setActiveTab('ip-whitelist')}
>
IP Control
</button>
</div>
) : (
<>
{/* Tabs */}
<div className="card">
<div className="border-b border-gray-200 dark:border-gray-700">
<nav className="-mb-px flex space-x-8">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`py-2 px-1 border-b-2 font-medium text-sm ${
activeTab === tab.id
? 'border-primary-500 text-primary-600 dark:text-primary-400'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300'
}`}
>
<span className="mr-2">{tab.icon}</span>
{tab.label}
</button>
))}
</nav>
</div>
<div className="security-panel">
<div className="p-6">
{/* API Keys Tab */}
{activeTab === 'api-keys' && (
<div className="security-section">
<div className="security-section-header">
<h2>API Keys</h2>
<button className="security-add-btn" onClick={handleCreateApiKey}>
Create New Key
<div className="space-y-4">
<div className="flex justify-between items-center">
<h3 className="text-lg font-medium text-gray-900 dark:text-white">API Keys</h3>
<button onClick={handleCreateApiKey} className="btn btn-primary">
<svg className="h-4 w-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
Create API Key
</button>
</div>
<div className="security-table">
<table>
<div className="overflow-x-auto">
<table className="table">
<thead>
<tr>
<th>Name</th>
@@ -340,29 +233,40 @@ const SecurityPage = () => {
<th>Created</th>
<th>Last Used</th>
<th>Status</th>
<th>Actions</th>
<th></th>
</tr>
</thead>
<tbody>
{apiKeys.map(key => (
<tr key={key.id}>
<td>{key.name}</td>
<td>{key.key}</td>
<td>{new Date(key.created).toLocaleDateString()}</td>
<td>{key.lastUsed ? new Date(key.lastUsed).toLocaleDateString() : 'Never'}</td>
{apiKeys.map((key) => (
<tr key={key.id} className="hover:bg-gray-50 dark:hover:bg-dark-surfaceHover transition-colors">
<td>
<span className={`status-badge ${key.status}`}>
<p className="font-medium text-gray-900 dark:text-white">{key.name}</p>
</td>
<td>
<code className="text-sm bg-gray-100 dark:bg-gray-800 px-2 py-1 rounded font-mono">
{key.key}
</code>
</td>
<td>
<p className="text-sm text-gray-600 dark:text-gray-400">{key.created}</p>
</td>
<td>
<p className="text-sm text-gray-600 dark:text-gray-400">{key.lastUsed}</p>
</td>
<td>
<span className={`badge ${key.status === 'active' ? 'badge-success' : 'badge-error'}`}>
{key.status}
</span>
</td>
<td>
<button
className="security-revoke-btn"
onClick={() => handleRevokeApiKey(key.id)}
disabled={key.status === 'revoked'}
>
Revoke
</button>
{key.status === 'active' && (
<button
onClick={() => handleRevokeApiKey(key.id)}
className="btn btn-ghost btn-sm text-error-600 hover:text-error-700"
>
Revoke
</button>
)}
</td>
</tr>
))}
@@ -372,39 +276,44 @@ const SecurityPage = () => {
</div>
)}
{/* Rate Limits Tab */}
{activeTab === 'rate-limits' && (
<div className="security-section">
<div className="security-section-header">
<h2>Rate Limits</h2>
<button className="security-add-btn" onClick={handleAddRateLimit}>
<div className="space-y-4">
<div className="flex justify-between items-center">
<h3 className="text-lg font-medium text-gray-900 dark:text-white">Rate Limits</h3>
<button onClick={handleAddRateLimit} className="btn btn-primary">
<svg className="h-4 w-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
Add Rate Limit
</button>
</div>
<div className="security-table">
<table>
<div className="overflow-x-auto">
<table className="table">
<thead>
<tr>
<th>Path</th>
<th>Limit</th>
<th>Window</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{rateLimits.map(limit => (
<tr key={limit.id}>
<td>{limit.path}</td>
<td>{limit.limit}</td>
<td>{limit.window}</td>
{rateLimits.map((limit) => (
<tr key={limit.id} className="hover:bg-gray-50 dark:hover:bg-dark-surfaceHover transition-colors">
<td>
<span className={`status-badge ${limit.status}`}>
{limit.status}
</span>
<p className="font-medium text-gray-900 dark:text-white">{limit.path}</p>
</td>
<td>
<button className="security-edit-btn">Edit</button>
<button className="security-delete-btn">Delete</button>
<p className="text-sm text-gray-600 dark:text-gray-400">{limit.limit}</p>
</td>
<td>
<p className="text-sm text-gray-600 dark:text-gray-400">{limit.window}</p>
</td>
<td>
<span className={`badge ${limit.status === 'active' ? 'badge-success' : 'badge-gray'}`}>
{limit.status}
</span>
</td>
</tr>
))}
@@ -414,39 +323,91 @@ const SecurityPage = () => {
</div>
)}
{/* IP Whitelist Tab */}
{activeTab === 'ip-whitelist' && (
<div className="security-section">
<div className="security-section-header">
<h2>IP Whitelist</h2>
<button className="security-add-btn" onClick={handleAddIpWhitelist}>
<div className="space-y-4">
<div className="flex justify-between items-center">
<h3 className="text-lg font-medium text-gray-900 dark:text-white">IP Whitelist</h3>
<button onClick={handleAddIpWhitelist} className="btn btn-primary">
<svg className="h-4 w-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
Add IP Address
</button>
</div>
<div className="security-table">
<table>
<div className="overflow-x-auto">
<table className="table">
<thead>
<tr>
<th>IP Address</th>
<th>Description</th>
<th>Created</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{ipWhitelist.map(ip => (
<tr key={ip.id}>
<td>{ip.ip}</td>
<td>{ip.description}</td>
<td>{new Date(ip.created).toLocaleDateString()}</td>
{ipWhitelist.map((ip) => (
<tr key={ip.id} className="hover:bg-gray-50 dark:hover:bg-dark-surfaceHover transition-colors">
<td>
<span className={`status-badge ${ip.status}`}>
<p className="font-medium text-gray-900 dark:text-white">{ip.ip}</p>
</td>
<td>
<p className="text-sm text-gray-600 dark:text-gray-400">{ip.description}</p>
</td>
<td>
<p className="text-sm text-gray-600 dark:text-gray-400">{ip.created}</p>
</td>
<td>
<span className={`badge ${ip.status === 'active' ? 'badge-success' : 'badge-gray'}`}>
{ip.status}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{/* Security Policies Tab */}
{activeTab === 'policies' && (
<div className="space-y-4">
<div className="flex justify-between items-center">
<h3 className="text-lg font-medium text-gray-900 dark:text-white">Security Policies</h3>
<button onClick={handleCreateSecurityPolicy} className="btn btn-primary">
<svg className="h-4 w-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
Create Policy
</button>
</div>
<div className="overflow-x-auto">
<table className="table">
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Created</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{securityPolicies.map((policy) => (
<tr key={policy.id} className="hover:bg-gray-50 dark:hover:bg-dark-surfaceHover transition-colors">
<td>
<button className="security-edit-btn">Edit</button>
<button className="security-delete-btn">Delete</button>
<p className="font-medium text-gray-900 dark:text-white">{policy.name}</p>
</td>
<td>
<span className="badge badge-primary">{policy.type}</span>
</td>
<td>
<p className="text-sm text-gray-600 dark:text-gray-400">{policy.createdAt}</p>
</td>
<td>
<span className={`badge ${policy.status === 'active' ? 'badge-success' : 'badge-gray'}`}>
{policy.status}
</span>
</td>
</tr>
))}
@@ -457,11 +418,11 @@ const SecurityPage = () => {
)}
</div>
</div>
)}
</main>
</>
)}
</div>
</>
);
};
</Layout>
)
}
export default SecurityPage;
export default SecurityPage
+258 -236
View File
@@ -1,45 +1,22 @@
'use client'
import React, { useState, useEffect } from 'react';
import Link from 'next/link';
import './settings.css';
import React, { useState, useEffect } from 'react'
import Layout from '@/components/Layout'
interface UserSettings {
username: string;
email: string;
currentPassword: string;
newPassword: string;
confirmPassword: string;
originalUsername: string;
originalEmail: string;
username: string
email: string
currentPassword: string
newPassword: string
confirmPassword: string
originalUsername: string
originalEmail: string
}
const menuItems = [
{ label: 'Dashboard', href: '/dashboard' },
{ label: 'APIs', href: '/apis' },
{ label: 'Routings', href: '/routings' },
{ label: 'Users', href: '/users' },
{ label: 'Groups', href: '/groups' },
{ label: 'Roles', href: '/roles' },
{ label: 'Monitor', href: '/monitor' },
{ label: 'Logs', href: '/logging' },
{ label: 'Security', href: '/security' },
{ label: 'Settings', href: '/settings' },
];
const handleLogout = () => {
localStorage.clear();
sessionStorage.clear();
setTimeout(() => {
window.location.replace('/');
}, 50);
};
const SettingsPage = () => {
const [theme, setTheme] = useState('light');
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null);
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [success, setSuccess] = useState<string | null>(null)
const [settings, setSettings] = useState<UserSettings>({
username: '',
email: '',
@@ -48,19 +25,16 @@ const SettingsPage = () => {
confirmPassword: '',
originalUsername: '',
originalEmail: '',
});
})
useEffect(() => {
const savedTheme = localStorage.getItem('theme') || 'light';
setTheme(savedTheme);
document.documentElement.setAttribute('data-theme', savedTheme);
fetchUserSettings();
}, []);
fetchUserSettings()
}, [])
const fetchUserSettings = async () => {
try {
setLoading(true);
setError(null);
setLoading(true)
setError(null)
const response = await fetch('http://localhost:3002/platform/user/me', {
credentials: 'include',
headers: {
@@ -68,240 +42,288 @@ const SettingsPage = () => {
'Content-Type': 'application/json',
'Cookie': `access_token_cookie=${document.cookie.split('; ').find(row => row.startsWith('access_token_cookie='))?.split('=')[1]}`
}
});
})
if (!response.ok) {
throw new Error('Failed to load user settings');
throw new Error('Failed to load user settings')
}
const data = await response.json();
const data = await response.json()
setSettings(prev => ({
...prev,
username: data.username,
email: data.email,
originalUsername: data.username,
originalEmail: data.email,
}));
}))
} catch (err) {
setError('Failed to load user settings. Please try again later.');
setError('Failed to load user settings. Please try again later.')
} finally {
setLoading(false);
setLoading(false)
}
};
}
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
const { name, value } = e.target
setSettings(prev => ({
...prev,
[name]: value
}));
};
}))
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
setSuccess(null);
e.preventDefault()
setError(null)
setSuccess(null)
if (settings.newPassword !== settings.confirmPassword) {
setError('New passwords do not match')
return
}
try {
setLoading(true);
// Update user info if username or email changed
if (settings.username !== settings.originalUsername || settings.email !== settings.originalEmail) {
const userInfoResponse = await fetch(`http://localhost:3002/platform/user/${settings.originalUsername}`, {
method: 'PUT',
credentials: 'include',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Cookie': `access_token_cookie=${document.cookie.split('; ').find(row => row.startsWith('access_token_cookie='))?.split('=')[1]}`
},
body: JSON.stringify({
username: settings.username,
email: settings.email
})
});
if (!userInfoResponse.ok) {
const errorData = await userInfoResponse.json();
throw new Error(errorData.error_message || 'Failed to update user info');
}
// Update original values after successful update
setSettings(prev => ({
...prev,
originalUsername: settings.username,
originalEmail: settings.email
}));
const updateData: any = {}
if (settings.username !== settings.originalUsername) {
updateData.username = settings.username
}
// Update password if new password is provided
if (settings.email !== settings.originalEmail) {
updateData.email = settings.email
}
if (settings.newPassword) {
if (settings.newPassword !== settings.confirmPassword) {
setError('New passwords do not match');
return;
}
const passwordResponse = await fetch(`http://localhost:3002/platform/user/${settings.username}/update-password`, {
method: 'PUT',
credentials: 'include',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Cookie': `access_token_cookie=${document.cookie.split('; ').find(row => row.startsWith('access_token_cookie='))?.split('=')[1]}`
},
body: JSON.stringify({
old_password: settings.currentPassword,
new_password: settings.newPassword
})
});
if (!passwordResponse.ok) {
const errorData = await passwordResponse.json();
throw new Error(errorData.error_message || 'Failed to update password');
}
updateData.current_password = settings.currentPassword
updateData.new_password = settings.newPassword
}
setSuccess('Settings updated successfully');
if (Object.keys(updateData).length === 0) {
setError('No changes to save')
return
}
const response = await fetch('http://localhost:3002/platform/user/update', {
method: 'PUT',
credentials: 'include',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Cookie': `access_token_cookie=${document.cookie.split('; ').find(row => row.startsWith('access_token_cookie='))?.split('=')[1]}`
},
body: JSON.stringify(updateData)
})
if (!response.ok) {
const errorData = await response.json()
throw new Error(errorData.detail || 'Failed to update settings')
}
setSuccess('Settings updated successfully!')
setSettings(prev => ({
...prev,
originalUsername: settings.username,
originalEmail: settings.email,
currentPassword: '',
newPassword: '',
confirmPassword: '',
}));
confirmPassword: ''
}))
setTimeout(() => setSuccess(null), 3000)
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to update settings. Please try again.');
} finally {
setLoading(false);
if (err instanceof Error) {
setError(err.message)
} else {
setError('Failed to update settings. Please try again.')
}
}
};
}
return (
<>
<div className="settings-topbar">
Doorman
</div>
<div className="settings-root">
<aside className="settings-sidebar">
<div className="settings-sidebar-title">Menu</div>
<ul className="settings-sidebar-list">
{menuItems.map((item, idx) => (
item.href ? (
<li key={item.label} className={`settings-sidebar-item${idx === 9 ? ' active' : ''}`}>
<Link href={item.href} style={{ color: 'inherit', textDecoration: 'none', display: 'block', width: '100%' }}>{item.label}</Link>
</li>
) : (
<li key={item.label} className={`settings-sidebar-item${idx === 9 ? ' active' : ''}`}>{item.label}</li>
)
))}
</ul>
<button className="settings-logout-btn" onClick={handleLogout}>
Logout
</button>
</aside>
<main className="settings-main">
<div className="settings-header-row">
<h1 className="settings-title">Settings</h1>
<Layout>
<div className="space-y-6">
{/* Page Header */}
<div className="page-header">
<div>
<h1 className="page-title">Settings</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">
Manage your account settings and preferences
</p>
</div>
</div>
{error && (
<div className="error-container">
<div className="error-message">
{error}
{/* Success Message */}
{success && (
<div className="rounded-lg bg-success-50 border border-success-200 p-4 dark:bg-success-900/20 dark:border-success-800">
<div className="flex">
<svg className="h-5 w-5 text-success-400 dark:text-success-500 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div className="ml-3">
<p className="text-sm text-success-700 dark:text-success-300">{success}</p>
</div>
</div>
)}
</div>
)}
{success && (
<div className="success-container">
<div className="success-message">
{success}
{/* Error Message */}
{error && (
<div className="rounded-lg bg-error-50 border border-error-200 p-4 dark:bg-error-900/20 dark:border-error-800">
<div className="flex">
<svg className="h-5 w-5 text-error-400 dark:text-error-500 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div className="ml-3">
<p className="text-sm text-error-700 dark:text-error-300">{error}</p>
</div>
</div>
)}
</div>
)}
{loading ? (
<div className="loading-spinner">
<div className="spinner"></div>
<p>Loading settings...</p>
{/* Loading State */}
{loading ? (
<div className="card">
<div className="flex items-center justify-center py-12">
<div className="text-center">
<div className="spinner mx-auto mb-4"></div>
<p className="text-gray-600 dark:text-gray-400">Loading settings...</p>
</div>
</div>
) : (
<div className="settings-panel">
<form onSubmit={handleSubmit} className="settings-form">
<div className="settings-section">
<h2>Account Information</h2>
<div className="form-group">
<label htmlFor="username">Username</label>
<input
type="text"
id="username"
name="username"
value={settings.username}
onChange={handleInputChange}
className="settings-input"
/>
</div>
<div className="form-group">
<label htmlFor="email">Email</label>
<input
type="email"
id="email"
name="email"
value={settings.email}
onChange={handleInputChange}
className="settings-input"
/>
</div>
</div>
<div className="settings-section">
<h2>Change Password</h2>
<div className="form-group">
<label htmlFor="currentPassword">Current Password</label>
<input
type="password"
id="currentPassword"
name="currentPassword"
value={settings.currentPassword}
onChange={handleInputChange}
className="settings-input"
/>
</div>
<div className="form-group">
<label htmlFor="newPassword">New Password</label>
<input
type="password"
id="newPassword"
name="newPassword"
value={settings.newPassword}
onChange={handleInputChange}
className="settings-input"
/>
</div>
<div className="form-group">
<label htmlFor="confirmPassword">Confirm New Password</label>
<input
type="password"
id="confirmPassword"
name="confirmPassword"
value={settings.confirmPassword}
onChange={handleInputChange}
className="settings-input"
/>
</div>
</div>
<div className="settings-actions">
<button type="submit" className="settings-save-btn">
Save Changes
</button>
</div>
<br />
<div className="update-warning">Warning: Any settings changes will log you out.</div>
</form>
</div>
) : (
/* Settings Form */
<div className="card max-w-2xl">
<div className="card-header">
<h3 className="card-title">Account Settings</h3>
</div>
)}
</main>
<form onSubmit={handleSubmit} className="p-6 space-y-6">
{/* Profile Information */}
<div className="space-y-4">
<h4 className="text-lg font-medium text-gray-900 dark:text-white">Profile Information</h4>
<div>
<label htmlFor="username" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Username
</label>
<input
type="text"
id="username"
name="username"
value={settings.username}
onChange={handleInputChange}
className="input"
placeholder="Enter your username"
disabled={loading}
/>
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Email Address
</label>
<input
type="email"
id="email"
name="email"
value={settings.email}
onChange={handleInputChange}
className="input"
placeholder="Enter your email address"
disabled={loading}
/>
</div>
</div>
{/* Password Change */}
<div className="space-y-4 pt-6 border-t border-gray-200 dark:border-gray-700">
<h4 className="text-lg font-medium text-gray-900 dark:text-white">Change Password</h4>
<div>
<label htmlFor="currentPassword" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Current Password
</label>
<input
type="password"
id="currentPassword"
name="currentPassword"
value={settings.currentPassword}
onChange={handleInputChange}
className="input"
placeholder="Enter your current password"
disabled={loading}
/>
</div>
<div>
<label htmlFor="newPassword" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
New Password
</label>
<input
type="password"
id="newPassword"
name="newPassword"
value={settings.newPassword}
onChange={handleInputChange}
className="input"
placeholder="Enter your new password"
disabled={loading}
/>
</div>
<div>
<label htmlFor="confirmPassword" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Confirm New Password
</label>
<input
type="password"
id="confirmPassword"
name="confirmPassword"
value={settings.confirmPassword}
onChange={handleInputChange}
className="input"
placeholder="Confirm your new password"
disabled={loading}
/>
</div>
</div>
{/* Submit Button */}
<div className="flex gap-4 pt-6 border-t border-gray-200 dark:border-gray-700">
<button
type="submit"
disabled={loading}
className="btn btn-primary flex-1"
>
{loading ? (
<div className="flex items-center justify-center">
<div className="spinner mr-2"></div>
Saving...
</div>
) : (
'Save Changes'
)}
</button>
<button
type="button"
onClick={() => {
setSettings(prev => ({
...prev,
username: prev.originalUsername,
email: prev.originalEmail,
currentPassword: '',
newPassword: '',
confirmPassword: ''
}))
setError(null)
}}
className="btn btn-secondary flex-1"
>
Reset
</button>
</div>
</form>
</div>
)}
</div>
</>
);
};
</Layout>
)
}
export default SettingsPage;
export default SettingsPage
File diff suppressed because it is too large Load Diff
+428 -388
View File
@@ -1,52 +1,30 @@
'use client'
import React, { useState } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import '../users.css';
import './add-user.css';
import React, { useState } from 'react'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
import Layout from '@/components/Layout'
interface CreateUserData {
username: string;
email: string;
password: string;
role: string;
groups: string[];
rate_limit_duration?: number;
rate_limit_duration_type?: string;
throttle_duration?: number;
throttle_duration_type?: string;
throttle_wait_duration?: number;
throttle_wait_duration_type?: string;
throttle_queue_limit?: number | null;
custom_attributes: Record<string, string>;
active: boolean;
ui_access: boolean;
username: string
email: string
password: string
role: string
groups: string[]
rate_limit_duration?: number
rate_limit_duration_type?: string
throttle_duration?: number
throttle_duration_type?: string
throttle_wait_duration?: number
throttle_wait_duration_type?: string
throttle_queue_limit?: number | null
custom_attributes: Record<string, string>
active: boolean
ui_access: boolean
}
const menuItems = [
{ label: 'Dashboard', href: '/dashboard' },
{ label: 'APIs', href: '/apis' },
{ label: 'Routings', href: '/routings' },
{ label: 'Users', href: '/users' },
{ label: 'Groups', href: '/groups' },
{ label: 'Roles', href: '/roles' },
{ label: 'Monitor', href: '/monitor' },
{ label: 'Logs', href: '/logging' },
{ label: 'Security', href: '/security' },
{ label: 'Settings', href: '/settings' },
];
const handleLogout = () => {
localStorage.clear();
sessionStorage.clear();
setTimeout(() => {
window.location.replace('/');
}, 50);
};
const AddUserPage = () => {
const router = useRouter();
const router = useRouter()
const [formData, setFormData] = useState<CreateUserData>({
username: '',
email: '',
@@ -56,49 +34,49 @@ const AddUserPage = () => {
custom_attributes: {},
active: true,
ui_access: false
});
const [newGroup, setNewGroup] = useState('');
const [newCustomAttribute, setNewCustomAttribute] = useState({ key: '', value: '' });
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [passwordStrength, setPasswordStrength] = useState({ score: 0, message: '' });
})
const [newGroup, setNewGroup] = useState('')
const [newCustomAttribute, setNewCustomAttribute] = useState({ key: '', value: '' })
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [passwordStrength, setPasswordStrength] = useState({ score: 0, message: '' })
const handleInputChange = (field: keyof CreateUserData, value: any) => {
setFormData(prev => ({ ...prev, [field]: value }));
setFormData(prev => ({ ...prev, [field]: value }))
// Check password strength when password changes
if (field === 'password') {
checkPasswordStrength(value);
checkPasswordStrength(value)
}
};
}
const checkPasswordStrength = (password: string) => {
let score = 0;
let message = '';
let score = 0
let message = ''
if (password.length >= 16) score++;
if (/[A-Z]/.test(password)) score++;
if (/[a-z]/.test(password)) score++;
if (/\d/.test(password)) score++;
if (/[!@#$%^&*(),.?":{}|<>]/.test(password)) score++;
if (password.length >= 16) score++
if (/[A-Z]/.test(password)) score++
if (/[a-z]/.test(password)) score++
if (/\d/.test(password)) score++
if (/[!@#$%^&*(),.?":{}|<>]/.test(password)) score++
if (score < 3) message = 'Weak - Password must include at least 16 characters, one uppercase letter, one lowercase letter, one digit, and one special character';
else if (score < 5) message = 'Medium - Add more complexity';
else message = 'Strong - Password meets security requirements';
if (score < 3) message = 'Weak - Password must include at least 16 characters, one uppercase letter, one lowercase letter, one digit, and one special character'
else if (score < 5) message = 'Medium - Add more complexity'
else message = 'Strong - Password meets security requirements'
setPasswordStrength({ score, message });
};
setPasswordStrength({ score, message })
}
const addGroup = () => {
if (newGroup.trim()) {
setFormData(prev => ({ ...prev, groups: [...prev.groups, newGroup.trim()] }));
setNewGroup('');
setFormData(prev => ({ ...prev, groups: [...prev.groups, newGroup.trim()] }))
setNewGroup('')
}
};
}
const removeGroup = (index: number) => {
setFormData(prev => ({ ...prev, groups: prev.groups.filter((_, i) => i !== index) }));
};
setFormData(prev => ({ ...prev, groups: prev.groups.filter((_, i) => i !== index) }))
}
const addCustomAttribute = () => {
if (newCustomAttribute.key && newCustomAttribute.value) {
@@ -108,34 +86,34 @@ const AddUserPage = () => {
...prev.custom_attributes,
[newCustomAttribute.key]: newCustomAttribute.value
}
}));
setNewCustomAttribute({ key: '', value: '' });
}))
setNewCustomAttribute({ key: '', value: '' })
}
};
}
const removeCustomAttribute = (key: string) => {
const newCustomAttributes = { ...formData.custom_attributes };
delete newCustomAttributes[key];
setFormData(prev => ({ ...prev, custom_attributes: newCustomAttributes }));
};
const newCustomAttributes = { ...formData.custom_attributes }
delete newCustomAttributes[key]
setFormData(prev => ({ ...prev, custom_attributes: newCustomAttributes }))
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
e.preventDefault()
// Validate required fields
if (!formData.username || !formData.email || !formData.password || !formData.role) {
setError('Please fill in all required fields');
return;
setError('Please fill in all required fields')
return
}
if (passwordStrength.score < 5) {
setError('Password does not meet security requirements');
return;
setError('Password does not meet security requirements')
return
}
try {
setLoading(true);
setError(null);
setLoading(true)
setError(null)
const response = await fetch('http://localhost:3002/platform/user/', {
method: 'POST',
@@ -146,336 +124,398 @@ const AddUserPage = () => {
'Cookie': `access_token_cookie=${document.cookie.split('; ').find(row => row.startsWith('access_token_cookie='))?.split('=')[1]}`
},
body: JSON.stringify(formData)
});
})
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error_message || 'Failed to create user');
const errorData = await response.json()
throw new Error(errorData.error_message || 'Failed to create user')
}
// Redirect to users list after successful creation
router.push('/users');
router.push('/users')
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to create user');
setError(err instanceof Error ? err.message : 'Failed to create user')
} finally {
setLoading(false);
setLoading(false)
}
};
const handleBack = () => {
router.back();
};
}
return (
<>
<div className="users-topbar">
Doorman
</div>
<div className="users-root">
<aside className="users-sidebar">
<div className="users-sidebar-title">Menu</div>
<ul className="users-sidebar-list">
{menuItems.map((item, idx) => (
item.href ? (
<li key={item.label} className={`users-sidebar-item${idx === 3 ? ' active' : ''}`}>
<Link href={item.href} style={{ color: 'inherit', textDecoration: 'none', display: 'block', width: '100%' }}>{item.label}</Link>
</li>
) : (
<li key={item.label} className={`users-sidebar-item${idx === 3 ? ' active' : ''}`}>{item.label}</li>
)
))}
</ul>
<button className="users-logout-btn" onClick={handleLogout}>
Logout
</button>
</aside>
<main className="users-main">
<div className="users-header-row">
<div className="add-user-header">
<button className="back-button" onClick={handleBack}>
<span className="back-arrow"></span>
Back to Users
</button>
<h1 className="users-title">Add New User</h1>
<Layout>
<div className="space-y-6">
{/* Page Header */}
<div className="page-header">
<div>
<h1 className="page-title">Add New User</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">
Create a new user account with permissions and settings
</p>
</div>
<Link href="/users" className="btn btn-secondary">
<svg className="h-4 w-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
Back to Users
</Link>
</div>
{/* Error Message */}
{error && (
<div className="rounded-lg bg-error-50 border border-error-200 p-4 dark:bg-error-900/20 dark:border-error-800">
<div className="flex">
<svg className="h-5 w-5 text-error-400 dark:text-error-500 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div className="ml-3">
<p className="text-sm text-error-700 dark:text-error-300">{error}</p>
</div>
</div>
</div>
)}
{error && (
<div className="error-container">
<div className="error-message">
{error}
{/* Form */}
<div className="card max-w-4xl">
<form onSubmit={handleSubmit} className="space-y-8">
{/* Basic Information */}
<div className="space-y-6">
<h3 className="text-lg font-medium text-gray-900 dark:text-white">Basic Information</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label htmlFor="username" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Username *
</label>
<input
type="text"
id="username"
className="input"
placeholder="Enter username"
value={formData.username}
onChange={(e) => handleInputChange('username', e.target.value)}
minLength={3}
maxLength={50}
required
disabled={loading}
/>
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Email *
</label>
<input
type="email"
id="email"
className="input"
placeholder="Enter email"
value={formData.email}
onChange={(e) => handleInputChange('email', e.target.value)}
required
disabled={loading}
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Password *
</label>
<input
type="password"
id="password"
className="input"
placeholder="Enter password (min 16 chars)"
value={formData.password}
onChange={(e) => handleInputChange('password', e.target.value)}
minLength={16}
maxLength={50}
required
disabled={loading}
/>
{formData.password && (
<div className={`mt-2 text-sm ${passwordStrength.score < 5 ? 'text-error-600 dark:text-error-400' : 'text-success-600 dark:text-success-400'}`}>
{passwordStrength.message}
</div>
)}
</div>
<div>
<label htmlFor="role" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Role *
</label>
<input
type="text"
id="role"
className="input"
placeholder="Enter role"
value={formData.role}
onChange={(e) => handleInputChange('role', e.target.value)}
minLength={2}
maxLength={50}
required
disabled={loading}
/>
</div>
<div>
<label htmlFor="status" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Status
</label>
<select
id="status"
className="input"
value={formData.active ? 'true' : 'false'}
onChange={(e) => handleInputChange('active', e.target.value === 'true')}
disabled={loading}
>
<option value="true">Active</option>
<option value="false">Inactive</option>
</select>
</div>
<div>
<label htmlFor="ui_access" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
UI Access
</label>
<select
id="ui_access"
className="input"
value={formData.ui_access ? 'true' : 'false'}
onChange={(e) => handleInputChange('ui_access', e.target.value === 'true')}
disabled={loading}
>
<option value="false">Disabled</option>
<option value="true">Enabled</option>
</select>
</div>
</div>
</div>
)}
<div className="add-user-content">
<form onSubmit={handleSubmit} className="add-user-form">
<div className="form-section">
<h2 className="section-title">Basic Information</h2>
<div className="form-grid">
<div className="form-group">
<label className="form-label">Username *</label>
<input
type="text"
className="form-input"
value={formData.username}
onChange={(e) => handleInputChange('username', e.target.value)}
placeholder="Enter username"
minLength={3}
maxLength={50}
required
/>
</div>
<div className="form-group">
<label className="form-label">Email *</label>
<input
type="email"
className="form-input"
value={formData.email}
onChange={(e) => handleInputChange('email', e.target.value)}
placeholder="Enter email"
required
/>
</div>
<div className="form-group">
<label className="form-label">Password *</label>
<input
type="password"
className="form-input"
value={formData.password}
onChange={(e) => handleInputChange('password', e.target.value)}
placeholder="Enter password (min 16 chars)"
minLength={16}
maxLength={50}
required
/>
{formData.password && (
<div className={`password-strength ${passwordStrength.score < 5 ? 'weak' : 'strong'}`}>
{passwordStrength.message}
</div>
)}
</div>
<div className="form-group">
<label className="form-label">Role *</label>
<input
type="text"
className="form-input"
value={formData.role}
onChange={(e) => handleInputChange('role', e.target.value)}
placeholder="Enter role"
minLength={2}
maxLength={50}
required
/>
</div>
<div className="form-group">
<label className="form-label">Status</label>
<select
className="form-select"
value={formData.active ? 'true' : 'false'}
onChange={(e) => handleInputChange('active', e.target.value === 'true')}
>
<option value="true">Active</option>
<option value="false">Inactive</option>
</select>
</div>
<div className="form-group">
<label className="form-label">UI Access</label>
<select
className="form-select"
value={formData.ui_access ? 'true' : 'false'}
onChange={(e) => handleInputChange('ui_access', e.target.value === 'true')}
>
<option value="false">Disabled</option>
<option value="true">Enabled</option>
</select>
</div>
</div>
</div>
<div className="form-section">
<h2 className="section-title">Groups</h2>
<div className="groups-container">
<div className="groups-list">
{formData.groups.map((group, index) => (
<span key={index} className="group-tag">
{group}
<button
type="button"
className="remove-group-btn"
onClick={() => removeGroup(index)}
>
×
</button>
</span>
))}
</div>
<div className="add-group">
<input
type="text"
className="form-input"
placeholder="Enter group name"
value={newGroup}
onChange={(e) => setNewGroup(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && (e.preventDefault(), addGroup())}
/>
<button type="button" className="add-button" onClick={addGroup}>
Add Group
</button>
</div>
</div>
</div>
<div className="form-section">
<h2 className="section-title">Rate Limiting</h2>
<div className="form-grid">
<div className="form-group">
<label className="form-label">Rate Limit Duration</label>
<input
type="number"
className="form-input"
value={formData.rate_limit_duration || ''}
onChange={(e) => handleInputChange('rate_limit_duration', e.target.value ? parseInt(e.target.value) : undefined)}
min="0"
placeholder="100"
/>
</div>
<div className="form-group">
<label className="form-label">Rate Limit Type</label>
<select
className="form-select"
value={formData.rate_limit_duration_type || ''}
onChange={(e) => handleInputChange('rate_limit_duration_type', e.target.value)}
>
<option value="">Select type</option>
<option value="second">Second</option>
<option value="minute">Minute</option>
<option value="hour">Hour</option>
<option value="day">Day</option>
</select>
</div>
</div>
</div>
<div className="form-section">
<h2 className="section-title">Throttling</h2>
<div className="form-grid">
<div className="form-group">
<label className="form-label">Throttle Duration</label>
<input
type="number"
className="form-input"
value={formData.throttle_duration || ''}
onChange={(e) => handleInputChange('throttle_duration', e.target.value ? parseInt(e.target.value) : undefined)}
min="0"
placeholder="10"
/>
</div>
<div className="form-group">
<label className="form-label">Throttle Type</label>
<select
className="form-select"
value={formData.throttle_duration_type || ''}
onChange={(e) => handleInputChange('throttle_duration_type', e.target.value)}
>
<option value="">Select type</option>
<option value="second">Second</option>
<option value="minute">Minute</option>
<option value="hour">Hour</option>
<option value="day">Day</option>
</select>
</div>
<div className="form-group">
<label className="form-label">Wait Duration</label>
<input
type="number"
className="form-input"
value={formData.throttle_wait_duration || ''}
onChange={(e) => handleInputChange('throttle_wait_duration', e.target.value ? parseInt(e.target.value) : undefined)}
min="0"
placeholder="5"
/>
</div>
<div className="form-group">
<label className="form-label">Wait Type</label>
<select
className="form-select"
value={formData.throttle_wait_duration_type || ''}
onChange={(e) => handleInputChange('throttle_wait_duration_type', e.target.value)}
>
<option value="">Select type</option>
<option value="second">Second</option>
<option value="minute">Minute</option>
<option value="hour">Hour</option>
<option value="day">Day</option>
</select>
</div>
<div className="form-group">
<label className="form-label">Queue Limit</label>
<input
type="number"
className="form-input"
value={formData.throttle_queue_limit || ''}
onChange={(e) => handleInputChange('throttle_queue_limit', e.target.value ? parseInt(e.target.value) : null)}
min="0"
placeholder="10"
/>
</div>
</div>
</div>
<div className="form-section">
<h2 className="section-title">Custom Attributes</h2>
<div className="custom-attributes-container">
{Object.entries(formData.custom_attributes).map(([key, value]) => (
<div key={key} className="custom-attribute-item">
<span className="custom-attribute-key">{key}:</span>
<span className="custom-attribute-value">{value}</span>
{/* Groups */}
<div className="space-y-6 pt-6 border-t border-gray-200 dark:border-gray-700">
<h3 className="text-lg font-medium text-gray-900 dark:text-white">Groups</h3>
<div className="space-y-4">
<div className="flex flex-wrap gap-2">
{formData.groups.map((group, index) => (
<div key={index} className="flex items-center gap-2 bg-blue-100 dark:bg-blue-900/20 text-blue-800 dark:text-blue-200 px-3 py-1 rounded-full">
<span className="text-sm">{group}</span>
<button
type="button"
className="remove-button"
onClick={() => removeCustomAttribute(key)}
onClick={() => removeGroup(index)}
className="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-200"
disabled={loading}
>
×
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
))}
<div className="add-custom-attribute">
<input
type="text"
className="form-input"
placeholder="Key"
value={newCustomAttribute.key}
onChange={(e) => setNewCustomAttribute(prev => ({ ...prev, key: e.target.value }))}
/>
<input
type="text"
className="form-input"
placeholder="Value"
value={newCustomAttribute.value}
onChange={(e) => setNewCustomAttribute(prev => ({ ...prev, value: e.target.value }))}
/>
<button type="button" className="add-button" onClick={addCustomAttribute}>
Add
</button>
</div>
</div>
<div className="flex gap-2">
<input
type="text"
className="input flex-1"
placeholder="Enter group name"
value={newGroup}
onChange={(e) => setNewGroup(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && (e.preventDefault(), addGroup())}
disabled={loading}
/>
<button type="button" className="btn btn-primary" onClick={addGroup} disabled={loading}>
Add Group
</button>
</div>
</div>
</div>
<div className="form-actions">
<button type="button" className="cancel-button" onClick={handleBack}>
Cancel
</button>
<button type="submit" className="save-button" disabled={loading}>
{loading ? 'Creating User...' : 'Create User'}
</button>
{/* Rate Limiting */}
<div className="space-y-6 pt-6 border-t border-gray-200 dark:border-gray-700">
<h3 className="text-lg font-medium text-gray-900 dark:text-white">Rate Limiting</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label htmlFor="rate_limit_duration" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Rate Limit Duration
</label>
<input
type="number"
id="rate_limit_duration"
className="input"
value={formData.rate_limit_duration || ''}
onChange={(e) => handleInputChange('rate_limit_duration', e.target.value ? parseInt(e.target.value) : undefined)}
min="0"
placeholder="100"
disabled={loading}
/>
</div>
<div>
<label htmlFor="rate_limit_duration_type" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Rate Limit Type
</label>
<select
id="rate_limit_duration_type"
className="input"
value={formData.rate_limit_duration_type || ''}
onChange={(e) => handleInputChange('rate_limit_duration_type', e.target.value)}
disabled={loading}
>
<option value="">Select type</option>
<option value="second">Second</option>
<option value="minute">Minute</option>
<option value="hour">Hour</option>
<option value="day">Day</option>
</select>
</div>
</div>
</form>
</div>
</main>
</div>
</>
);
};
</div>
export default AddUserPage;
{/* Throttling */}
<div className="space-y-6 pt-6 border-t border-gray-200 dark:border-gray-700">
<h3 className="text-lg font-medium text-gray-900 dark:text-white">Throttling</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label htmlFor="throttle_duration" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Throttle Duration
</label>
<input
type="number"
id="throttle_duration"
className="input"
value={formData.throttle_duration || ''}
onChange={(e) => handleInputChange('throttle_duration', e.target.value ? parseInt(e.target.value) : undefined)}
min="0"
placeholder="10"
disabled={loading}
/>
</div>
<div>
<label htmlFor="throttle_duration_type" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Throttle Type
</label>
<select
id="throttle_duration_type"
className="input"
value={formData.throttle_duration_type || ''}
onChange={(e) => handleInputChange('throttle_duration_type', e.target.value)}
disabled={loading}
>
<option value="">Select type</option>
<option value="second">Second</option>
<option value="minute">Minute</option>
<option value="hour">Hour</option>
<option value="day">Day</option>
</select>
</div>
<div>
<label htmlFor="throttle_wait_duration" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Wait Duration
</label>
<input
type="number"
id="throttle_wait_duration"
className="input"
value={formData.throttle_wait_duration || ''}
onChange={(e) => handleInputChange('throttle_wait_duration', e.target.value ? parseInt(e.target.value) : undefined)}
min="0"
placeholder="5"
disabled={loading}
/>
</div>
<div>
<label htmlFor="throttle_wait_duration_type" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Wait Type
</label>
<select
id="throttle_wait_duration_type"
className="input"
value={formData.throttle_wait_duration_type || ''}
onChange={(e) => handleInputChange('throttle_wait_duration_type', e.target.value)}
disabled={loading}
>
<option value="">Select type</option>
<option value="second">Second</option>
<option value="minute">Minute</option>
<option value="hour">Hour</option>
<option value="day">Day</option>
</select>
</div>
<div>
<label htmlFor="throttle_queue_limit" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Queue Limit
</label>
<input
type="number"
id="throttle_queue_limit"
className="input"
value={formData.throttle_queue_limit || ''}
onChange={(e) => handleInputChange('throttle_queue_limit', e.target.value ? parseInt(e.target.value) : null)}
min="0"
placeholder="10"
disabled={loading}
/>
</div>
</div>
</div>
{/* Custom Attributes */}
<div className="space-y-6 pt-6 border-t border-gray-200 dark:border-gray-700">
<h3 className="text-lg font-medium text-gray-900 dark:text-white">Custom Attributes</h3>
<div className="space-y-4">
<div className="flex flex-wrap gap-2">
{Object.entries(formData.custom_attributes).map(([key, value]) => (
<div key={key} className="flex items-center gap-2 bg-purple-100 dark:bg-purple-900/20 text-purple-800 dark:text-purple-200 px-3 py-1 rounded-full">
<span className="text-sm">{key}: {value}</span>
<button
type="button"
onClick={() => removeCustomAttribute(key)}
className="text-purple-600 hover:text-purple-800 dark:text-purple-400 dark:hover:text-purple-200"
disabled={loading}
>
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
))}
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-2">
<input
type="text"
className="input"
placeholder="Key"
value={newCustomAttribute.key}
onChange={(e) => setNewCustomAttribute(prev => ({ ...prev, key: e.target.value }))}
disabled={loading}
/>
<input
type="text"
className="input"
placeholder="Value"
value={newCustomAttribute.value}
onChange={(e) => setNewCustomAttribute(prev => ({ ...prev, value: e.target.value }))}
disabled={loading}
/>
<button type="button" className="btn btn-primary" onClick={addCustomAttribute} disabled={loading}>
Add
</button>
</div>
</div>
</div>
{/* Form Actions */}
<div className="flex gap-4 pt-6 border-t border-gray-200 dark:border-gray-700">
<button type="submit" disabled={loading} className="btn btn-primary flex-1">
{loading ? (
<div className="flex items-center justify-center">
<div className="spinner mr-2"></div>
Creating User...
</div>
) : (
'Create User'
)}
</button>
<Link href="/users" className="btn btn-secondary flex-1">
Cancel
</Link>
</div>
</form>
</div>
</div>
</Layout>
)
}
export default AddUserPage
+205 -176
View File
@@ -1,258 +1,287 @@
'use client'
import React, { useState, useEffect } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import './users.css';
import React, { useState, useEffect } from 'react'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
import Layout from '@/components/Layout'
interface User {
username: string;
email: string;
role: string;
groups: string[];
rate_limit_duration: number;
rate_limit_duration_type: string;
throttle_duration: number;
throttle_duration_type: string;
throttle_wait_duration: number;
throttle_wait_duration_type: string;
throttle_queue_limit: number | null;
custom_attributes: Record<string, string>;
active: boolean;
username: string
email: string
is_active: boolean
created_at: string
last_login?: string
roles?: string[]
}
const menuItems = [
{ label: 'Dashboard', href: '/dashboard' },
{ label: 'APIs', href: '/apis' },
{ label: 'Routings', href: '/routings' },
{ label: 'Users', href: '/users' },
{ label: 'Groups', href: '/groups' },
{ label: 'Roles', href: '/roles' },
{ label: 'Monitor', href: '/monitor' },
{ label: 'Logs', href: '/logging' },
{ label: 'Security', href: '/security' },
{ label: 'Settings', href: '/settings' },
];
const handleLogout = () => {
localStorage.clear();
sessionStorage.clear();
setTimeout(() => {
window.location.replace('/');
}, 50);
};
const UsersPage = () => {
const router = useRouter();
const [users, setUsers] = useState<User[]>([]);
const [allUsers, setAllUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [searchTerm, setSearchTerm] = useState('');
const [sortBy, setSortBy] = useState('username');
useEffect(() => {}, []);
const router = useRouter()
const [users, setUsers] = useState<User[]>([])
const [allUsers, setAllUsers] = useState<User[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [searchTerm, setSearchTerm] = useState('')
const [sortBy, setSortBy] = useState('username')
useEffect(() => {
fetchUsers();
}, []);
fetchUsers()
}, [])
const fetchUsers = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch(`http://localhost:3002/platform/user/all?page=1&page_size=10`, {
setLoading(true)
setError(null)
const response = await fetch(`http://localhost:3002/platform/user/all`, {
credentials: 'include',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Cookie': `access_token_cookie=${document.cookie.split('; ').find(row => row.startsWith('access_token_cookie='))?.split('=')[1]}`
}
});
})
if (!response.ok) {
throw new Error('Failed to load users');
throw new Error('Failed to load users')
}
const data = await response.json();
setAllUsers(data.users);
setUsers(data.users);
const data = await response.json()
const userList = Array.isArray(data) ? data : (data.users || data.response?.users || [])
setAllUsers(userList)
setUsers(userList)
} catch (err) {
setError('Failed to load users. Please try again later.');
setUsers([]);
setAllUsers([]);
setError('Failed to load users. Please try again later.')
setUsers([])
setAllUsers([])
} finally {
setLoading(false);
setLoading(false)
}
};
}
const handleSearch = (e: React.FormEvent) => {
e.preventDefault();
e.preventDefault()
if (!searchTerm.trim()) {
setUsers(allUsers);
return;
setUsers(allUsers)
return
}
const filteredUsers = allUsers.filter(user =>
user.username.toLowerCase().includes(searchTerm.toLowerCase()) ||
user.email.toLowerCase().includes(searchTerm.toLowerCase()) ||
user.role.toLowerCase().includes(searchTerm.toLowerCase()) ||
user.groups.some(group => group.toLowerCase().includes(searchTerm.toLowerCase()))
);
setUsers(filteredUsers);
};
(user.roles || []).some(role => role.toLowerCase().includes(searchTerm.toLowerCase()))
)
setUsers(filteredUsers)
}
const handleSort = (sortField: string) => {
setSortBy(sortField);
setSortBy(sortField)
const sortedUsers = [...users].sort((a, b) => {
if (sortField === 'username') {
return a.username.localeCompare(b.username);
} else if (sortField === 'role') {
return a.role.localeCompare(b.role);
return a.username.localeCompare(b.username)
} else if (sortField === 'email') {
return a.email.localeCompare(b.email)
} else if (sortField === 'status') {
return a.active ? 1 : -1;
return a.is_active === b.is_active ? 0 : a.is_active ? -1 : 1
}
return 0;
});
setUsers(sortedUsers);
};
return 0
})
setUsers(sortedUsers)
}
const handleUserClick = (user: User) => {
// Store user data in sessionStorage for the detail page
sessionStorage.setItem('selectedUser', JSON.stringify(user));
router.push(`/users/${user.username}`);
};
sessionStorage.setItem('selectedUser', JSON.stringify(user))
router.push(`/users/${user.username}`)
}
const formatDuration = (duration: number | null | undefined, durationType: string | null | undefined) => {
if (!duration || !durationType) return 'Not set';
const plural = duration !== 1 && (durationType.endsWith('minute') || durationType.endsWith('second') || durationType.endsWith('hour')) ? 's' : '';
return `${duration} ${durationType}${plural}`;
};
const formatDate = (dateString: string) => {
if (!dateString) return 'Never'
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
})
}
return (
<>
<div className="users-topbar">
Doorman
</div>
<div className="users-root">
<aside className="users-sidebar">
<div className="users-sidebar-title">Menu</div>
<ul className="users-sidebar-list">
{menuItems.map((item, idx) => (
item.href ? (
<li key={item.label} className={`users-sidebar-item${idx === 3 ? ' active' : ''}`}>
<Link href={item.href} style={{ color: 'inherit', textDecoration: 'none', display: 'block', width: '100%' }}>{item.label}</Link>
</li>
) : (
<li key={item.label} className={`users-sidebar-item${idx === 3 ? ' active' : ''}`}>{item.label}</li>
)
))}
</ul>
<button className="users-logout-btn" onClick={handleLogout}>
Logout
</button>
</aside>
<main className="users-main">
<div className="users-header-row">
<h1 className="users-title">Users</h1>
<button className="refresh-button" onClick={fetchUsers}>
<span className="refresh-icon"></span>
Refresh
</button>
<Layout>
<div className="space-y-6">
{/* Page Header */}
<div className="page-header">
<div>
<h1 className="page-title">Users</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">
Manage user accounts and permissions
</p>
</div>
<Link href="/users/add" className="btn btn-primary">
<svg className="h-4 w-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
Add User
</Link>
</div>
<div className="users-controls">
<form className="users-search-box" onSubmit={handleSearch}>
<input
type="text"
className="users-search-input"
placeholder="Search users..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
<button type="submit" className="users-search-btn">Search</button>
<Link href="/users/add" className="users-add-btn" style={{ textDecoration: 'none', display: 'inline-block' }}>
Add User
</Link>
{/* Search and Filters */}
<div className="card">
<div className="flex flex-col sm:flex-row gap-4">
<form onSubmit={handleSearch} className="flex-1">
<div className="relative">
<svg className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
<input
type="text"
className="search-input"
placeholder="Search users by username, email, or role..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
</form>
<div className="users-sort-group">
<button
className={`users-sort-btn ${sortBy === 'username' ? 'active' : ''}`}
<div className="flex gap-2">
<button
onClick={() => handleSort('username')}
className={`btn ${sortBy === 'username' ? 'btn-primary' : 'btn-secondary'}`}
>
Name
Username
</button>
<button
className={`users-sort-btn ${sortBy === 'role' ? 'active' : ''}`}
onClick={() => handleSort('role')}
<button
onClick={() => handleSort('email')}
className={`btn ${sortBy === 'email' ? 'btn-primary' : 'btn-secondary'}`}
>
Role
Email
</button>
<button
className={`users-sort-btn ${sortBy === 'status' ? 'active' : ''}`}
<button
onClick={() => handleSort('status')}
className={`btn ${sortBy === 'status' ? 'btn-primary' : 'btn-secondary'}`}
>
Status
</button>
</div>
</div>
</div>
{error && (
<div className="error-container">
<div className="error-message">
{error}
{/* Error Message */}
{error && (
<div className="rounded-lg bg-error-50 border border-error-200 p-4 dark:bg-error-900/20 dark:border-error-800">
<div className="flex">
<svg className="h-5 w-5 text-error-400 dark:text-error-500 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div className="ml-3">
<p className="text-sm text-error-700 dark:text-error-300">{error}</p>
</div>
</div>
)}
</div>
)}
{loading ? (
<div className="loading-spinner">
<div className="spinner"></div>
<p>Loading users...</p>
{/* Loading State */}
{loading ? (
<div className="card">
<div className="flex items-center justify-center py-12">
<div className="text-center">
<div className="spinner mx-auto mb-4"></div>
<p className="text-gray-600 dark:text-gray-400">Loading users...</p>
</div>
</div>
) : (
<div className="users-table-panel">
<table className="users-table">
</div>
) : (
/* Users Table */
<div className="card">
<div className="overflow-x-auto">
<table className="table">
<thead>
<tr>
<th>Username</th>
<th>Email</th>
<th>Role</th>
<th>Groups</th>
<th>Rate Limit</th>
<th>Throttle</th>
<th>Roles</th>
<th>Status</th>
<th>Last Login</th>
<th></th>
</tr>
</thead>
<tbody>
{users.map((user) => (
<tr
key={user.username}
key={user.username}
onClick={() => handleUserClick(user)}
style={{ cursor: 'pointer' }}
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#f5f5f5'}
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = ''}
className="cursor-pointer hover:bg-gray-50 dark:hover:bg-dark-surfaceHover transition-colors"
>
<td>{user.username}</td>
<td>{user.email}</td>
<td>{user.role}</td>
<td>{user.groups.length > 0 ? user.groups[0] : ''}</td>
<td>{formatDuration(user.rate_limit_duration, user.rate_limit_duration_type)}</td>
<td>{formatDuration(user.throttle_duration, user.throttle_duration_type)}</td>
<td>
<span className={`status-badge ${user.active ? 'active' : 'inactive'}`}>
{user.active ? 'Active' : 'Inactive'}
<div className="flex items-center">
<div className="h-10 w-10 rounded-full bg-gradient-to-br from-primary-400 to-primary-600 flex items-center justify-center text-white font-medium">
{user.username.charAt(0).toUpperCase()}
</div>
<div className="ml-3">
<p className="font-medium text-gray-900 dark:text-white">
{user.username}
</p>
</div>
</div>
</td>
<td>
<p className="text-sm text-gray-900 dark:text-white">{user.email}</p>
</td>
<td>
<div className="flex flex-wrap gap-1">
{(user.roles || []).slice(0, 2).map((role, index) => (
<span key={index} className="badge badge-primary text-xs">
{role}
</span>
))}
{(user.roles || []).length > 2 && (
<span className="badge badge-gray text-xs">
+{(user.roles || []).length - 2}
</span>
)}
</div>
</td>
<td>
<span className={`badge ${user.is_active ? 'badge-success' : 'badge-error'}`}>
{user.is_active ? 'Active' : 'Inactive'}
</span>
</td>
<td>
<p className="text-sm text-gray-500 dark:text-gray-400">
{formatDate(user.last_login || '')}
</p>
</td>
<td>
<button className="btn btn-ghost btn-sm">
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</main>
</div>
</>
);
};
export default UsersPage;
{/* Empty State */}
{users.length === 0 && !loading && (
<div className="text-center py-12">
<div className="h-16 w-16 mx-auto mb-4 rounded-full bg-gray-100 dark:bg-gray-800 flex items-center justify-center">
<svg className="h-8 w-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z" />
</svg>
</div>
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">No users found</h3>
<p className="text-gray-600 dark:text-gray-400 mb-4">
{searchTerm ? 'Try adjusting your search terms.' : 'Get started by creating your first user account.'}
</p>
<Link href="/users/add" className="btn btn-primary">
<svg className="h-4 w-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
Add User
</Link>
</div>
)}
</div>
)}
</div>
</Layout>
)
}
export default UsersPage
+145
View File
@@ -0,0 +1,145 @@
'use client'
import React, { useState, useEffect } from 'react'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
interface LayoutProps {
children: React.ReactNode
}
const menuItems = [
{ label: 'Dashboard', href: '/dashboard', icon: '📊' },
{ label: 'APIs', href: '/apis', icon: '🔌' },
{ label: 'Routings', href: '/routings', icon: '🛣️' },
{ label: 'Users', href: '/users', icon: '👥' },
{ label: 'Groups', href: '/groups', icon: '👨‍👩‍👧‍👦' },
{ label: 'Roles', href: '/roles', icon: '🔐' },
{ label: 'Monitor', href: '/monitor', icon: '📈' },
{ label: 'Logs', href: '/logging', icon: '📝' },
{ label: 'Security', href: '/security', icon: '🛡️' },
{ label: 'Settings', href: '/settings', icon: '⚙️' },
]
const Layout: React.FC<LayoutProps> = ({ children }) => {
const [theme, setTheme] = useState('light')
const [sidebarOpen, setSidebarOpen] = useState(false)
const pathname = usePathname()
useEffect(() => {
const savedTheme = localStorage.getItem('theme') || 'light'
setTheme(savedTheme)
document.documentElement.classList.toggle('dark', savedTheme === 'dark')
}, [])
const toggleTheme = () => {
const newTheme = theme === 'light' ? 'dark' : 'light'
setTheme(newTheme)
localStorage.setItem('theme', newTheme)
document.documentElement.classList.toggle('dark', newTheme === 'dark')
}
const handleLogout = () => {
localStorage.clear()
sessionStorage.clear()
document.cookie = 'access_token_cookie=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'
window.location.href = '/login'
}
return (
<div className="min-h-screen bg-gray-50 dark:bg-dark-bg">
{/* Topbar */}
<header className="topbar">
<div className="flex h-full items-center justify-between px-4 lg:px-6">
<div className="flex items-center gap-4">
<button
onClick={() => setSidebarOpen(!sidebarOpen)}
className="lg:hidden rounded-lg p-2 text-gray-500 hover:bg-gray-100 dark:text-dark-textSecondary dark:hover:bg-dark-surfaceHover"
>
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
<div className="flex items-center gap-2">
<div className="h-8 w-8 rounded-lg bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center">
<span className="text-white font-bold text-sm">D</span>
</div>
<span className="text-xl font-bold gradient-text">Doorman</span>
</div>
</div>
<div className="flex items-center gap-3">
<button
onClick={toggleTheme}
className="rounded-lg p-2 text-gray-500 hover:bg-gray-100 dark:text-dark-textSecondary dark:hover:bg-dark-surfaceHover transition-colors"
aria-label="Toggle theme"
>
{theme === 'light' ? (
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
</svg>
) : (
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
)}
</button>
<div className="h-8 w-px bg-gray-200 dark:bg-dark-border" />
<button
onClick={handleLogout}
className="btn btn-ghost text-sm"
>
<svg className="h-4 w-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
</svg>
Logout
</button>
</div>
</div>
</header>
{/* Sidebar */}
<aside className={`sidebar ${sidebarOpen ? 'translate-x-0' : ''}`}>
<div className="flex h-full flex-col">
<div className="flex-1 space-y-1 p-4">
<nav className="space-y-2">
{menuItems.map((item) => {
const isActive = pathname === item.href
return (
<Link
key={item.href}
href={item.href}
className={`sidebar-item ${isActive ? 'active' : ''}`}
onClick={() => setSidebarOpen(false)}
>
<span className="mr-3 text-lg">{item.icon}</span>
{item.label}
</Link>
)
})}
</nav>
</div>
</div>
</aside>
{/* Overlay for mobile */}
{sidebarOpen && (
<div
className="fixed inset-0 z-30 bg-black/50 lg:hidden"
onClick={() => setSidebarOpen(false)}
/>
)}
{/* Main content */}
<main className="main-content">
<div className="p-4 lg:p-6">
{children}
</div>
</main>
</div>
)
}
export default Layout
+111 -2
View File
@@ -6,11 +6,120 @@ export default {
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
],
darkMode: 'class',
theme: {
extend: {
colors: {
background: "var(--background)",
foreground: "var(--foreground)",
// Light mode colors
primary: {
50: '#f0f9ff',
100: '#e0f2fe',
200: '#bae6fd',
300: '#7dd3fc',
400: '#38bdf8',
500: '#0ea5e9',
600: '#0284c7',
700: '#0369a1',
800: '#075985',
900: '#0c4a6e',
950: '#082f49',
},
gray: {
50: '#f9fafb',
100: '#f3f4f6',
200: '#e5e7eb',
300: '#d1d5db',
400: '#9ca3af',
500: '#6b7280',
600: '#4b5563',
700: '#374151',
800: '#1f2937',
900: '#111827',
950: '#030712',
},
success: {
50: '#f0fdf4',
100: '#dcfce7',
200: '#bbf7d0',
300: '#86efac',
400: '#4ade80',
500: '#22c55e',
600: '#16a34a',
700: '#15803d',
800: '#166534',
900: '#14532d',
},
warning: {
50: '#fffbeb',
100: '#fef3c7',
200: '#fde68a',
300: '#fcd34d',
400: '#fbbf24',
500: '#f59e0b',
600: '#d97706',
700: '#b45309',
800: '#92400e',
900: '#78350f',
},
error: {
50: '#fef2f2',
100: '#fee2e2',
200: '#fecaca',
300: '#fca5a5',
400: '#f87171',
500: '#ef4444',
600: '#dc2626',
700: '#b91c1c',
800: '#991b1b',
900: '#7f1d1d',
},
// Dark mode specific colors
dark: {
bg: '#0a0a0a',
surface: '#1a1a1a',
surfaceHover: '#2a2a2a',
border: '#333333',
text: '#ffffff',
textSecondary: '#a1a1aa',
}
},
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
mono: ['JetBrains Mono', 'monospace'],
},
animation: {
'fade-in': 'fadeIn 0.5s ease-in-out',
'slide-up': 'slideUp 0.3s ease-out',
'slide-down': 'slideDown 0.3s ease-out',
'scale-in': 'scaleIn 0.2s ease-out',
'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
},
keyframes: {
fadeIn: {
'0%': { opacity: '0' },
'100%': { opacity: '1' },
},
slideUp: {
'0%': { transform: 'translateY(10px)', opacity: '0' },
'100%': { transform: 'translateY(0)', opacity: '1' },
},
slideDown: {
'0%': { transform: 'translateY(-10px)', opacity: '0' },
'100%': { transform: 'translateY(0)', opacity: '1' },
},
scaleIn: {
'0%': { transform: 'scale(0.95)', opacity: '0' },
'100%': { transform: 'scale(1)', opacity: '1' },
},
},
boxShadow: {
'soft': '0 2px 15px -3px rgba(0, 0, 0, 0.07), 0 10px 20px -2px rgba(0, 0, 0, 0.04)',
'medium': '0 4px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)',
'strong': '0 10px 40px -10px rgba(0, 0, 0, 0.15), 0 2px 10px -2px rgba(0, 0, 0, 0.05)',
'glow': '0 0 20px rgba(59, 130, 246, 0.15)',
},
backdropBlur: {
xs: '2px',
},
},
},