refactor: remove security-related hooks and pages

- Deleted `useSecurity.ts` hook which managed authentication users, providers, and policies.
- Removed `Policies.tsx`, `Providers.tsx`, and `Users.tsx` pages that utilized the above hook.
- Cleaned up the `index.tsx` file in the Security section to remove references to the deleted pages.
- Updated mock data by removing unused properties related to forward authentication.
This commit is contained in:
Wikid82
2025-11-26 00:02:15 +00:00
parent b20522f150
commit 4f03021c9c
37 changed files with 42 additions and 4264 deletions
-2
View File
@@ -13,7 +13,6 @@ const ProxyHosts = lazy(() => import('./pages/ProxyHosts'))
const RemoteServers = lazy(() => import('./pages/RemoteServers'))
const ImportCaddy = lazy(() => import('./pages/ImportCaddy'))
const Certificates = lazy(() => import('./pages/Certificates'))
const Security = lazy(() => import('./pages/Security'))
const SystemSettings = lazy(() => import('./pages/SystemSettings'))
const Account = lazy(() => import('./pages/Account'))
const Settings = lazy(() => import('./pages/Settings'))
@@ -51,7 +50,6 @@ export default function App() {
<Route path="uptime" element={<Uptime />} />
<Route path="notifications" element={<Notifications />} />
<Route path="import" element={<ImportCaddy />} />
<Route path="security" element={<Security />} />
{/* Settings Routes */}
<Route path="settings" element={<Settings />}>
@@ -37,8 +37,6 @@ describe('proxyHosts API', () => {
hsts_subdomains: false,
block_exploits: false,
websocket_support: false,
forward_auth_enabled: false,
forward_auth_bypass: '',
locations: [],
enabled: true,
created_at: '2023-01-01',
-3
View File
@@ -30,9 +30,6 @@ export interface ProxyHost {
hsts_subdomains: boolean;
block_exploits: boolean;
websocket_support: boolean;
forward_auth_enabled: boolean;
forward_auth_bypass: string;
auth_policy_id?: number | null;
locations: Location[];
advanced_config?: string;
enabled: boolean;
-240
View File
@@ -1,240 +0,0 @@
import client from './client';
// --- Forward Auth (Legacy) ---
export interface ForwardAuthConfig {
id?: number;
provider: 'authelia' | 'authentik' | 'pomerium' | 'custom';
address: string;
trust_forward_header: boolean;
created_at?: string;
updated_at?: string;
}
export interface ForwardAuthTemplate {
provider: string;
address: string;
trust_forward_header: boolean;
description: string;
}
export const getForwardAuthConfig = async (): Promise<ForwardAuthConfig> => {
const { data } = await client.get<ForwardAuthConfig>('/security/forward-auth');
return data;
};
export const updateForwardAuthConfig = async (config: ForwardAuthConfig): Promise<ForwardAuthConfig> => {
const { data } = await client.put<ForwardAuthConfig>('/security/forward-auth', config);
return data;
};
export const getForwardAuthTemplates = async (): Promise<Record<string, ForwardAuthTemplate>> => {
const { data } = await client.get<Record<string, ForwardAuthTemplate>>('/security/forward-auth/templates');
return data;
};
// --- Built-in SSO ---
// Users
export interface AuthUser {
id: number;
uuid: string;
username: string;
email: string;
name: string;
password?: string; // Only for creation/update
roles: string;
mfa_enabled: boolean;
enabled: boolean;
created_at: string;
updated_at: string;
additional_emails?: string;
}
export interface AuthUserStats {
total_users: number;
admin_users: number;
}
export interface CreateAuthUserRequest {
username: string;
email: string;
name: string;
password?: string;
roles: string;
mfa_enabled: boolean;
additional_emails?: string;
}
export interface UpdateAuthUserRequest {
email?: string;
name?: string;
password?: string;
roles?: string;
mfa_enabled?: boolean;
enabled?: boolean;
additional_emails?: string;
}
export const getAuthUsers = async (): Promise<AuthUser[]> => {
const { data } = await client.get<AuthUser[]>('/security/users');
return data;
};
export const getAuthUser = async (uuid: string): Promise<AuthUser> => {
const { data } = await client.get<AuthUser>(`/security/users/${uuid}`);
return data;
};
export const createAuthUser = async (user: CreateAuthUserRequest): Promise<AuthUser> => {
const { data } = await client.post<AuthUser>('/security/users', user);
return data;
};
export const updateAuthUser = async (uuid: string, user: UpdateAuthUserRequest): Promise<AuthUser> => {
const { data } = await client.put<AuthUser>(`/security/users/${uuid}`, user);
return data;
};
export const deleteAuthUser = async (uuid: string): Promise<void> => {
await client.delete(`/security/users/${uuid}`);
};
export const getAuthUserStats = async (): Promise<AuthUserStats> => {
const { data } = await client.get<AuthUserStats>('/security/users/stats');
return data;
};
// Providers
export interface AuthProvider {
id: number;
uuid: string;
name: string;
type: 'google' | 'github' | 'oidc';
client_id: string;
client_secret?: string; // Only for creation/update
issuer_url?: string;
auth_url?: string;
token_url?: string;
user_info_url?: string;
scopes?: string;
role_mapping?: string;
display_name?: string;
enabled: boolean;
created_at: string;
updated_at: string;
}
export interface CreateAuthProviderRequest {
name: string;
type: 'google' | 'github' | 'oidc';
client_id: string;
client_secret: string;
issuer_url?: string;
auth_url?: string;
token_url?: string;
user_info_url?: string;
scopes?: string;
role_mapping?: string;
display_name?: string;
}
export interface UpdateAuthProviderRequest {
name?: string;
type?: 'google' | 'github' | 'oidc';
client_id?: string;
client_secret?: string;
issuer_url?: string;
auth_url?: string;
token_url?: string;
user_info_url?: string;
scopes?: string;
role_mapping?: string;
display_name?: string;
enabled?: boolean;
}
export const getAuthProviders = async (): Promise<AuthProvider[]> => {
const { data } = await client.get<AuthProvider[]>('/security/providers');
return data;
};
export const getAuthProvider = async (uuid: string): Promise<AuthProvider> => {
const { data } = await client.get<AuthProvider>(`/security/providers/${uuid}`);
return data;
};
export const createAuthProvider = async (provider: CreateAuthProviderRequest): Promise<AuthProvider> => {
const { data } = await client.post<AuthProvider>('/security/providers', provider);
return data;
};
export const updateAuthProvider = async (uuid: string, provider: UpdateAuthProviderRequest): Promise<AuthProvider> => {
const { data } = await client.put<AuthProvider>(`/security/providers/${uuid}`, provider);
return data;
};
export const deleteAuthProvider = async (uuid: string): Promise<void> => {
await client.delete(`/security/providers/${uuid}`);
};
// Policies
export interface AuthPolicy {
id: number;
uuid: string;
name: string;
description: string;
allowed_roles: string;
allowed_users: string;
allowed_domains: string;
require_mfa: boolean;
session_timeout: number;
enabled: boolean;
created_at: string;
updated_at: string;
}
export interface CreateAuthPolicyRequest {
name: string;
description?: string;
allowed_roles?: string;
allowed_users?: string;
allowed_domains?: string;
require_mfa?: boolean;
session_timeout?: number;
}
export interface UpdateAuthPolicyRequest {
name?: string;
description?: string;
allowed_roles?: string;
allowed_users?: string;
allowed_domains?: string;
require_mfa?: boolean;
session_timeout?: number;
enabled?: boolean;
}
export const getAuthPolicies = async (): Promise<AuthPolicy[]> => {
const { data } = await client.get<AuthPolicy[]>('/security/policies');
return data;
};
export const getAuthPolicy = async (uuid: string): Promise<AuthPolicy> => {
const { data } = await client.get<AuthPolicy>(`/security/policies/${uuid}`);
return data;
};
export const createAuthPolicy = async (policy: CreateAuthPolicyRequest): Promise<AuthPolicy> => {
const { data } = await client.post<AuthPolicy>('/security/policies', policy);
return data;
};
export const updateAuthPolicy = async (uuid: string, policy: UpdateAuthPolicyRequest): Promise<AuthPolicy> => {
const { data } = await client.put<AuthPolicy>(`/security/policies/${uuid}`, policy);
return data;
};
export const deleteAuthPolicy = async (uuid: string): Promise<void> => {
await client.delete(`/security/policies/${uuid}`);
};
@@ -1,157 +0,0 @@
import { useState, useEffect } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { getForwardAuthConfig, updateForwardAuthConfig, getForwardAuthTemplates, ForwardAuthConfig } from '../api/security';
import { Button } from './ui/Button';
import { toast } from 'react-hot-toast';
import { Shield, Check, AlertTriangle } from 'lucide-react';
export default function ForwardAuthSettings() {
const queryClient = useQueryClient();
const [formData, setFormData] = useState<ForwardAuthConfig>({
provider: 'custom',
address: '',
trust_forward_header: true,
});
const { data: config, isLoading } = useQuery({
queryKey: ['forwardAuth'],
queryFn: getForwardAuthConfig,
});
const { data: templates } = useQuery({
queryKey: ['forwardAuthTemplates'],
queryFn: getForwardAuthTemplates,
});
useEffect(() => {
if (config) {
setFormData(config);
}
}, [config]);
const mutation = useMutation({
mutationFn: updateForwardAuthConfig,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['forwardAuth'] });
toast.success('Forward Auth configuration saved');
},
onError: (error: Error & { response?: { data?: { error?: string } } }) => {
toast.error(error.response?.data?.error || 'Failed to save configuration');
},
});
const handleTemplateChange = (provider: string) => {
if (templates && templates[provider]) {
const template = templates[provider];
setFormData({
...formData,
provider: provider as 'authelia' | 'authentik' | 'pomerium' | 'custom',
address: template.address,
trust_forward_header: template.trust_forward_header,
});
} else {
setFormData({
...formData,
provider: 'custom',
});
}
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
mutation.mutate(formData);
};
if (isLoading) {
return <div className="animate-pulse h-64 bg-gray-100 dark:bg-gray-800 rounded-lg"></div>;
}
return (
<div className="bg-white dark:bg-dark-card rounded-lg shadow-sm border border-gray-200 dark:border-gray-800 p-6">
<div className="flex items-center gap-3 mb-6">
<div className="p-2 bg-blue-100 dark:bg-blue-900/30 rounded-lg">
<Shield className="w-6 h-6 text-blue-600 dark:text-blue-400" />
</div>
<div>
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">Forward Authentication</h2>
<p className="text-sm text-gray-500 dark:text-gray-400">
Configure a global authentication provider (SSO) for your proxy hosts.
</p>
</div>
</div>
<form onSubmit={handleSubmit} className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Provider Template
</label>
<select
value={formData.provider}
onChange={(e) => handleTemplateChange(e.target.value)}
className="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-dark-bg text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="custom">Custom</option>
<option value="authelia">Authelia</option>
<option value="authentik">Authentik</option>
<option value="pomerium">Pomerium</option>
</select>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
Select a template to pre-fill configuration or choose Custom.
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Auth Service Address
</label>
<input
type="url"
required
value={formData.address}
onChange={(e) => setFormData({ ...formData, address: e.target.value })}
placeholder="http://authelia:9091/api/verify"
className="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-dark-bg text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
The internal URL where Caddy will send auth subrequests.
</p>
</div>
</div>
<div className="flex items-center gap-3 p-4 bg-gray-50 dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-gray-700">
<input
type="checkbox"
id="trust_forward_header"
checked={formData.trust_forward_header}
onChange={(e) => setFormData({ ...formData, trust_forward_header: e.target.checked })}
className="w-4 h-4 text-blue-600 rounded border-gray-300 focus:ring-blue-500 dark:bg-dark-bg dark:border-gray-600"
/>
<label htmlFor="trust_forward_header" className="flex-1">
<span className="block text-sm font-medium text-gray-900 dark:text-white">
Trust Forward Headers
</span>
<span className="block text-xs text-gray-500 dark:text-gray-400">
Send X-Forwarded-Method and X-Forwarded-Uri headers to the auth service. Required for most providers.
</span>
</label>
</div>
<div className="flex items-center justify-between pt-4 border-t border-gray-200 dark:border-gray-800">
<div className="flex items-center gap-2 text-sm text-amber-600 dark:text-amber-400">
<AlertTriangle className="w-4 h-4" />
<span>Changes apply immediately to all hosts using Forward Auth.</span>
</div>
<Button
type="submit"
isLoading={mutation.isPending}
className="flex items-center gap-2"
>
<Check className="w-4 h-4" />
Save Configuration
</Button>
</div>
</form>
</div>
);
}
-1
View File
@@ -50,7 +50,6 @@ export default function Layout({ children }: LayoutProps) {
{ name: 'Uptime', path: '/uptime', icon: '📈' },
{ name: 'Notifications', path: '/notifications', icon: '🔔' },
{ name: 'Import Caddyfile', path: '/import', icon: '📥' },
{ name: 'Security', path: '/security', icon: '🛡️' },
{
name: 'Settings',
path: '/settings',
+1 -82
View File
@@ -1,12 +1,11 @@
import { useState, useEffect } from 'react'
import { CircleHelp, AlertCircle, Check, X, Loader2, ShieldCheck } from 'lucide-react'
import { CircleHelp, AlertCircle, Check, X, Loader2 } from 'lucide-react'
import type { ProxyHost } from '../api/proxyHosts'
import { testProxyHostConnection } from '../api/proxyHosts'
import { useRemoteServers } from '../hooks/useRemoteServers'
import { useDomains } from '../hooks/useDomains'
import { useCertificates } from '../hooks/useCertificates'
import { useDocker } from '../hooks/useDocker'
import { useAuthPolicies } from '../hooks/useSecurity'
import { parse } from 'tldts'
interface ProxyHostFormProps {
@@ -28,9 +27,6 @@ export default function ProxyHostForm({ host, onSubmit, onCancel }: ProxyHostFor
hsts_subdomains: host?.hsts_subdomains ?? true,
block_exploits: host?.block_exploits ?? true,
websocket_support: host?.websocket_support ?? true,
forward_auth_enabled: host?.forward_auth_enabled ?? false,
forward_auth_bypass: host?.forward_auth_bypass || '',
auth_policy_id: host?.auth_policy_id || null,
advanced_config: host?.advanced_config || '',
enabled: host?.enabled ?? true,
certificate_id: host?.certificate_id,
@@ -39,7 +35,6 @@ export default function ProxyHostForm({ host, onSubmit, onCancel }: ProxyHostFor
const { servers: remoteServers } = useRemoteServers()
const { domains, createDomain } = useDomains()
const { certificates } = useCertificates()
const { policies: authPolicies } = useAuthPolicies()
const { containers: dockerContainers, isLoading: dockerLoading, error: dockerError } = useDocker(
formData.forward_host ? undefined : undefined // Simplified for now, logic below handles it
)
@@ -489,82 +484,6 @@ export default function ProxyHostForm({ host, onSubmit, onCancel }: ProxyHostFor
</label>
</div>
{/* Access Control (SSO & Forward Auth) */}
<div className="p-4 bg-gray-800/50 rounded-lg border border-gray-700 space-y-4">
<div className="flex items-center gap-3 mb-2">
<ShieldCheck className="text-blue-400" size={20} />
<h3 className="text-lg font-medium text-white">Access Control</h3>
</div>
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
Access Policy (Built-in SSO)
</label>
<select
value={formData.auth_policy_id || ''}
onChange={e => {
const val = e.target.value ? parseInt(e.target.value) : null;
setFormData({
...formData,
auth_policy_id: val,
// If a policy is selected, disable legacy forward auth to avoid conflicts
forward_auth_enabled: val ? false : formData.forward_auth_enabled
});
}}
className="w-full bg-gray-900 border border-gray-700 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="">Public (No Authentication)</option>
{authPolicies.map(policy => (
<option key={policy.id} value={policy.id}>
{policy.name} {policy.description ? `(${policy.description})` : ''}
</option>
))}
</select>
<p className="text-xs text-gray-500 mt-1">
Select a policy to protect this service with the built-in SSO.
</p>
</div>
{/* Legacy Forward Auth - Only show if no policy is selected */}
{!formData.auth_policy_id && (
<div className="pt-4 border-t border-gray-700">
<div className="flex items-center justify-between">
<label className="flex items-center gap-3">
<input
type="checkbox"
checked={formData.forward_auth_enabled}
onChange={e => setFormData({ ...formData, forward_auth_enabled: e.target.checked })}
className="w-4 h-4 text-blue-600 bg-gray-900 border-gray-700 rounded focus:ring-blue-500"
/>
<span className="text-sm font-medium text-gray-300">Enable External Forward Auth</span>
</label>
<div title="Protects this service using your configured global authentication provider (e.g. Authelia, Authentik)." className="text-gray-500 hover:text-gray-300 cursor-help">
<CircleHelp size={14} />
</div>
</div>
{formData.forward_auth_enabled && (
<div className="mt-3">
<label htmlFor="forward-auth-bypass" className="block text-sm font-medium text-gray-300 mb-2">
Bypass Paths (Optional)
</label>
<textarea
id="forward-auth-bypass"
value={formData.forward_auth_bypass}
onChange={e => setFormData({ ...formData, forward_auth_bypass: e.target.value })}
placeholder="/api/webhook, /public/*"
rows={2}
className="w-full bg-gray-900 border border-gray-700 rounded-lg px-4 py-2 text-white font-mono text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<p className="text-xs text-gray-500 mt-1">
Comma-separated list of paths to exclude from authentication.
</p>
</div>
)}
</div>
)}
</div>
{/* Advanced Config */}
<div>
<label htmlFor="advanced-config" className="block text-sm font-medium text-gray-300 mb-2">
@@ -66,7 +66,6 @@ describe('Layout', () => {
expect(screen.getByText('Remote Servers')).toBeInTheDocument()
expect(screen.getByText('Certificates')).toBeInTheDocument()
expect(screen.getByText('Import Caddyfile')).toBeInTheDocument()
expect(screen.getByText('Security')).toBeInTheDocument()
expect(screen.getByText('Settings')).toBeInTheDocument()
})
@@ -234,25 +234,4 @@ describe('ProxyHostForm', () => {
expect(screen.getByLabelText(/Domain Names/i)).toHaveValue('my-app.existing.com')
})
it('toggles forward auth fields', async () => {
renderWithClient(
<ProxyHostForm onSubmit={mockOnSubmit} onCancel={mockOnCancel} />
)
// The Forward Auth toggle now uses "Enable External Forward Auth" label
// and only appears when no Access Policy is selected (default is no policy)
const toggle = screen.getByLabelText('Enable External Forward Auth')
expect(toggle).not.toBeChecked()
// Bypass field should not be visible initially
expect(screen.queryByLabelText('Bypass Paths (Optional)')).not.toBeInTheDocument()
// Enable it
fireEvent.click(toggle)
expect(toggle).toBeChecked()
// Bypass field should now be visible
expect(screen.getByLabelText('Bypass Paths (Optional)')).toBeInTheDocument()
})
})
@@ -26,8 +26,6 @@ const createMockHost = (overrides: Partial<api.ProxyHost> = {}): api.ProxyHost =
hsts_subdomains: false,
block_exploits: false,
websocket_support: false,
forward_auth_enabled: false,
forward_auth_bypass: '',
locations: [],
enabled: true,
created_at: '2025-01-01T00:00:00Z',
-134
View File
@@ -1,134 +0,0 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import * as api from '../api/security';
// Users Hooks
export const useAuthUsers = () => {
const queryClient = useQueryClient();
const { data: users = [], isLoading, error } = useQuery({
queryKey: ['auth-users'],
queryFn: api.getAuthUsers,
});
const { data: stats, isLoading: statsLoading } = useQuery({
queryKey: ['auth-users-stats'],
queryFn: api.getAuthUserStats,
});
const createMutation = useMutation({
mutationFn: api.createAuthUser,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['auth-users'] });
queryClient.invalidateQueries({ queryKey: ['auth-users-stats'] });
},
});
const updateMutation = useMutation({
mutationFn: ({ uuid, data }: { uuid: string; data: api.UpdateAuthUserRequest }) =>
api.updateAuthUser(uuid, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['auth-users'] });
queryClient.invalidateQueries({ queryKey: ['auth-users-stats'] });
},
});
const deleteMutation = useMutation({
mutationFn: api.deleteAuthUser,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['auth-users'] });
queryClient.invalidateQueries({ queryKey: ['auth-users-stats'] });
},
});
return {
users,
stats,
isLoading: isLoading || statsLoading,
error,
createUser: createMutation.mutateAsync,
updateUser: updateMutation.mutateAsync,
deleteUser: deleteMutation.mutateAsync,
};
};
// Providers Hooks
export const useAuthProviders = () => {
const queryClient = useQueryClient();
const { data: providers = [], isLoading, error } = useQuery({
queryKey: ['auth-providers'],
queryFn: api.getAuthProviders,
});
const createMutation = useMutation({
mutationFn: api.createAuthProvider,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['auth-providers'] });
},
});
const updateMutation = useMutation({
mutationFn: ({ uuid, data }: { uuid: string; data: api.UpdateAuthProviderRequest }) =>
api.updateAuthProvider(uuid, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['auth-providers'] });
},
});
const deleteMutation = useMutation({
mutationFn: api.deleteAuthProvider,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['auth-providers'] });
},
});
return {
providers,
isLoading,
error,
createProvider: createMutation.mutateAsync,
updateProvider: updateMutation.mutateAsync,
deleteProvider: deleteMutation.mutateAsync,
};
};
// Policies Hooks
export const useAuthPolicies = () => {
const queryClient = useQueryClient();
const { data: policies = [], isLoading, error } = useQuery({
queryKey: ['auth-policies'],
queryFn: api.getAuthPolicies,
});
const createMutation = useMutation({
mutationFn: api.createAuthPolicy,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['auth-policies'] });
},
});
const updateMutation = useMutation({
mutationFn: ({ uuid, data }: { uuid: string; data: api.UpdateAuthPolicyRequest }) =>
api.updateAuthPolicy(uuid, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['auth-policies'] });
},
});
const deleteMutation = useMutation({
mutationFn: api.deleteAuthPolicy,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['auth-policies'] });
},
});
return {
policies,
isLoading,
error,
createPolicy: createMutation.mutateAsync,
updatePolicy: updateMutation.mutateAsync,
deletePolicy: deleteMutation.mutateAsync,
};
};
-272
View File
@@ -1,272 +0,0 @@
import { useState } from 'react';
import { useAuthPolicies } from '../../hooks/useSecurity';
import { Button } from '../../components/ui/Button';
import { Plus, Edit, Trash2, ShieldCheck, Users, Globe } from 'lucide-react';
import toast from 'react-hot-toast';
import type { AuthPolicy, CreateAuthPolicyRequest, UpdateAuthPolicyRequest } from '../../api/security';
interface PolicyFormData {
name: string;
description: string;
allowed_roles: string;
allowed_users: string;
allowed_domains: string;
require_mfa: boolean;
session_timeout: number;
}
export default function Policies() {
const { policies, createPolicy, updatePolicy, deletePolicy, isLoading } = useAuthPolicies();
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingPolicy, setEditingPolicy] = useState<AuthPolicy | null>(null);
const [formData, setFormData] = useState<PolicyFormData>({
name: '',
description: '',
allowed_roles: '',
allowed_users: '',
allowed_domains: '',
require_mfa: false,
session_timeout: 0,
});
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
if (editingPolicy) {
const updateData: UpdateAuthPolicyRequest = {
name: formData.name,
description: formData.description,
allowed_roles: formData.allowed_roles,
allowed_users: formData.allowed_users,
allowed_domains: formData.allowed_domains,
require_mfa: formData.require_mfa,
session_timeout: formData.session_timeout,
};
await updatePolicy({ uuid: editingPolicy.uuid, data: updateData });
toast.success('Policy updated successfully');
} else {
const createData: CreateAuthPolicyRequest = {
name: formData.name,
description: formData.description,
allowed_roles: formData.allowed_roles,
allowed_users: formData.allowed_users,
allowed_domains: formData.allowed_domains,
require_mfa: formData.require_mfa,
session_timeout: formData.session_timeout,
};
await createPolicy(createData);
toast.success('Policy created successfully');
}
setIsModalOpen(false);
resetForm();
} catch (error: unknown) {
const err = error as { response?: { data?: { error?: string } } };
toast.error(err.response?.data?.error || 'Failed to save policy');
}
};
const handleDelete = async (uuid: string) => {
if (confirm('Are you sure you want to delete this policy?')) {
try {
await deletePolicy(uuid);
toast.success('Policy deleted successfully');
} catch (error: unknown) {
const err = error as { response?: { data?: { error?: string } } };
toast.error(err.response?.data?.error || 'Failed to delete policy');
}
}
};
const resetForm = () => {
setFormData({
name: '',
description: '',
allowed_roles: '',
allowed_users: '',
allowed_domains: '',
require_mfa: false,
session_timeout: 0,
});
setEditingPolicy(null);
};
const openEditModal = (policy: AuthPolicy) => {
setEditingPolicy(policy);
setFormData({
name: policy.name,
description: policy.description || '',
allowed_roles: policy.allowed_roles || '',
allowed_users: policy.allowed_users || '',
allowed_domains: policy.allowed_domains || '',
require_mfa: policy.require_mfa || false,
session_timeout: policy.session_timeout || 0,
});
setIsModalOpen(true);
};
if (isLoading) return <div>Loading...</div>;
return (
<div className="space-y-6">
<div className="flex justify-between items-center">
<h2 className="text-xl font-semibold text-white">Access Policies</h2>
<Button onClick={() => { resetForm(); setIsModalOpen(true); }}>
<Plus size={16} className="mr-2" />
Add Policy
</Button>
</div>
<div className="grid grid-cols-1 gap-4">
{policies.map((policy) => {
return (
<div key={policy.uuid} className="bg-dark-card rounded-lg border border-gray-800 p-6 flex flex-col md:flex-row justify-between gap-6">
<div className="space-y-2 flex-1">
<div className="flex items-center gap-3">
<h3 className="font-medium text-white text-lg">{policy.name}</h3>
{policy.require_mfa && (
<span className="px-2 py-0.5 rounded text-xs bg-blue-900/30 text-blue-400 border border-blue-900/50 flex items-center gap-1">
<ShieldCheck size={12} /> MFA Required
</span>
)}
</div>
<p className="text-sm text-gray-400">{policy.description || 'No description provided.'}</p>
<div className="flex flex-wrap gap-4 mt-4">
{policy.allowed_roles && (
<div className="flex items-center gap-2 text-sm text-gray-400">
<ShieldCheck size={16} className="text-gray-500" />
<span>Roles: <span className="text-gray-300">{policy.allowed_roles}</span></span>
</div>
)}
{policy.allowed_users && (
<div className="flex items-center gap-2 text-sm text-gray-400">
<Users size={16} className="text-gray-500" />
<span>Users: <span className="text-gray-300">{policy.allowed_users}</span></span>
</div>
)}
{policy.allowed_domains && (
<div className="flex items-center gap-2 text-sm text-gray-400">
<Globe size={16} className="text-gray-500" />
<span>Domains: <span className="text-gray-300">{policy.allowed_domains}</span></span>
</div>
)}
{!policy.allowed_roles && !policy.allowed_users && !policy.allowed_domains && (
<span className="text-sm text-yellow-500">Public Access (No restrictions)</span>
)}
</div>
</div>
<div className="flex items-start gap-2">
<button
onClick={() => openEditModal(policy)}
className="p-2 rounded-md hover:bg-gray-700 text-gray-400 hover:text-white transition-colors"
>
<Edit size={18} />
</button>
<button
onClick={() => handleDelete(policy.uuid)}
className="p-2 rounded-md hover:bg-red-900/30 text-gray-400 hover:text-red-400 transition-colors"
>
<Trash2 size={18} />
</button>
</div>
</div>
);
})}
{policies.length === 0 && (
<div className="text-center py-12 text-gray-500 bg-dark-card rounded-lg border border-gray-800 border-dashed">
No access policies defined. Create one to protect your services.
</div>
)}
</div>
{/* Policy Modal */}
{isModalOpen && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50">
<div className="bg-dark-card rounded-lg border border-gray-800 max-w-2xl w-full p-6 max-h-[90vh] overflow-y-auto">
<h3 className="text-lg font-bold text-white mb-4">
{editingPolicy ? 'Edit Policy' : 'Add Policy'}
</h3>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-400 mb-1">Policy Name</label>
<input
type="text"
required
value={formData.name}
onChange={e => setFormData({ ...formData, name: e.target.value })}
placeholder="Admins Only"
className="w-full bg-gray-900 border border-gray-700 rounded-lg px-3 py-2 text-white focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-400 mb-1">Description</label>
<textarea
value={formData.description}
onChange={e => setFormData({ ...formData, description: e.target.value })}
rows={2}
className="w-full bg-gray-900 border border-gray-700 rounded-lg px-3 py-2 text-white focus:ring-2 focus:ring-blue-500"
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-400 mb-1">Allowed Roles</label>
<input
type="text"
value={formData.allowed_roles}
onChange={e => setFormData({ ...formData, allowed_roles: e.target.value })}
placeholder="admin, editor"
className="w-full bg-gray-900 border border-gray-700 rounded-lg px-3 py-2 text-white focus:ring-2 focus:ring-blue-500"
/>
<p className="text-xs text-gray-500 mt-1">Comma-separated list of roles</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-400 mb-1">Allowed Users</label>
<input
type="text"
value={formData.allowed_users}
onChange={e => setFormData({ ...formData, allowed_users: e.target.value })}
placeholder="john, jane@example.com"
className="w-full bg-gray-900 border border-gray-700 rounded-lg px-3 py-2 text-white focus:ring-2 focus:ring-blue-500"
/>
<p className="text-xs text-gray-500 mt-1">Comma-separated usernames/emails</p>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-400 mb-1">Allowed Domains</label>
<input
type="text"
value={formData.allowed_domains}
onChange={e => setFormData({ ...formData, allowed_domains: e.target.value })}
placeholder="example.com, corp.net"
className="w-full bg-gray-900 border border-gray-700 rounded-lg px-3 py-2 text-white focus:ring-2 focus:ring-blue-500"
/>
<p className="text-xs text-gray-500 mt-1">Restrict access to users with these email domains</p>
</div>
<div className="flex items-center gap-4 pt-2">
<div className="flex items-center gap-2">
<input
type="checkbox"
id="require_mfa"
checked={formData.require_mfa}
onChange={e => setFormData({ ...formData, require_mfa: e.target.checked })}
className="w-4 h-4 text-blue-600 bg-gray-900 border-gray-700 rounded focus:ring-blue-500"
/>
<label htmlFor="require_mfa" className="text-sm text-gray-400">Require MFA</label>
</div>
</div>
<div className="flex justify-end gap-3 mt-6">
<Button variant="ghost" onClick={() => setIsModalOpen(false)}>Cancel</Button>
<Button type="submit">{editingPolicy ? 'Save Changes' : 'Create Policy'}</Button>
</div>
</form>
</div>
</div>
)}
</div>
);
}
-381
View File
@@ -1,381 +0,0 @@
import { useState, useRef } from 'react';
import { useAuthProviders } from '../../hooks/useSecurity';
import { Button } from '../../components/ui/Button';
import { Plus, Edit, Trash2, Globe } from 'lucide-react';
import toast from 'react-hot-toast';
import type { AuthProvider, CreateAuthProviderRequest, UpdateAuthProviderRequest } from '../../api/security';
interface ProviderFormData {
name: string;
type: 'google' | 'github' | 'oidc';
client_id: string;
client_secret: string;
issuer_url: string;
auth_url: string;
token_url: string;
user_info_url: string;
scopes: string;
display_name: string;
enabled: boolean;
}
interface HelpTooltipProps {
content: React.ReactNode;
position?: 'left' | 'right';
}
const HelpTooltip = ({ content, position = 'left' }: HelpTooltipProps) => {
const [isOpen, setIsOpen] = useState(false);
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const handleMouseEnter = () => {
if (timeoutRef.current) clearTimeout(timeoutRef.current);
setIsOpen(true);
};
const handleMouseLeave = () => {
timeoutRef.current = setTimeout(() => {
setIsOpen(false);
}, 300);
};
return (
<div className="relative inline-block" onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
<button
type="button"
className="w-4 h-4 rounded-full bg-gray-700 text-gray-400 hover:bg-gray-600 flex items-center justify-center text-xs cursor-help"
onClick={(e) => e.preventDefault()}
>
?
</button>
{isOpen && (
<div
className={`absolute bottom-6 ${position === 'left' ? 'left-0' : 'right-0'} bg-gray-800 text-white text-xs rounded-lg px-3 py-2 w-72 z-10 shadow-lg border border-gray-700`}
>
{content}
<div className={`absolute top-full ${position === 'left' ? 'left-1' : 'right-1'} border-4 border-transparent border-t-gray-800`}></div>
</div>
)}
</div>
);
};
export default function Providers() {
const { providers, createProvider, updateProvider, deleteProvider, isLoading } = useAuthProviders();
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingProvider, setEditingProvider] = useState<AuthProvider | null>(null);
const [formData, setFormData] = useState<ProviderFormData>({
name: '',
type: 'oidc',
client_id: '',
client_secret: '',
issuer_url: '',
auth_url: '',
token_url: '',
user_info_url: '',
scopes: 'openid,profile,email',
display_name: '',
enabled: true,
});
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
if (editingProvider) {
const updateData: UpdateAuthProviderRequest = {
name: formData.name,
client_id: formData.client_id,
issuer_url: formData.issuer_url,
auth_url: formData.auth_url,
token_url: formData.token_url,
user_info_url: formData.user_info_url,
scopes: formData.scopes,
display_name: formData.display_name,
enabled: formData.enabled,
};
if (formData.client_secret) {
updateData.client_secret = formData.client_secret;
}
await updateProvider({ uuid: editingProvider.uuid, data: updateData });
toast.success('Provider updated successfully');
} else {
const createData: CreateAuthProviderRequest = {
name: formData.name,
type: formData.type,
client_id: formData.client_id,
client_secret: formData.client_secret,
issuer_url: formData.issuer_url,
auth_url: formData.auth_url,
token_url: formData.token_url,
user_info_url: formData.user_info_url,
scopes: formData.scopes,
display_name: formData.display_name,
};
await createProvider(createData);
toast.success('Provider created successfully');
}
setIsModalOpen(false);
resetForm();
} catch (error: unknown) {
const err = error as { response?: { data?: { error?: string } } };
toast.error(err.response?.data?.error || 'Failed to save provider');
}
};
const handleDelete = async (uuid: string) => {
if (confirm('Are you sure you want to delete this provider?')) {
try {
await deleteProvider(uuid);
toast.success('Provider deleted successfully');
} catch (error: unknown) {
const err = error as { response?: { data?: { error?: string } } };
toast.error(err.response?.data?.error || 'Failed to delete provider');
}
}
};
const resetForm = () => {
setFormData({
name: '',
type: 'oidc',
client_id: '',
client_secret: '',
issuer_url: '',
auth_url: '',
token_url: '',
user_info_url: '',
scopes: 'openid,profile,email',
display_name: '',
enabled: true,
});
setEditingProvider(null);
};
const openEditModal = (provider: AuthProvider) => {
setEditingProvider(provider);
setFormData({
name: provider.name,
type: provider.type,
client_id: provider.client_id,
client_secret: '', // Don't populate secret
issuer_url: provider.issuer_url || '',
auth_url: provider.auth_url || '',
token_url: provider.token_url || '',
user_info_url: provider.user_info_url || '',
scopes: provider.scopes || 'openid,profile,email',
display_name: provider.display_name || '',
enabled: provider.enabled,
});
setIsModalOpen(true);
};
if (isLoading) return <div>Loading...</div>;
return (
<div className="space-y-6">
<div className="flex justify-between items-center">
<h2 className="text-xl font-semibold text-white">Identity Providers</h2>
<Button onClick={() => { resetForm(); setIsModalOpen(true); }}>
<Plus size={16} className="mr-2" />
Add Provider
</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{providers.map((provider) => (
<div key={provider.uuid} className="bg-dark-card rounded-lg border border-gray-800 p-6 space-y-4">
<div className="flex justify-between items-start">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-purple-900/30 flex items-center justify-center text-purple-400">
<Globe size={20} />
</div>
<div>
<h3 className="font-medium text-white">{provider.name}</h3>
<p className="text-xs text-gray-500 uppercase">{provider.type}</p>
</div>
</div>
<div className="flex gap-2">
<button
onClick={() => openEditModal(provider)}
className="p-1.5 rounded-md hover:bg-gray-700 text-gray-400 hover:text-white transition-colors"
>
<Edit size={16} />
</button>
<button
onClick={() => handleDelete(provider.uuid)}
className="p-1.5 rounded-md hover:bg-red-900/30 text-gray-400 hover:text-red-400 transition-colors"
>
<Trash2 size={16} />
</button>
</div>
</div>
<div className="space-y-2 text-sm text-gray-400">
<div className="flex justify-between">
<span>Client ID:</span>
<span className="font-mono text-gray-300">{provider.client_id}</span>
</div>
<div className="flex justify-between">
<span>Status:</span>
{provider.enabled ? (
<span className="text-green-400">Active</span>
) : (
<span className="text-red-400">Disabled</span>
)}
</div>
</div>
</div>
))}
{providers.length === 0 && (
<div className="col-span-full text-center py-12 text-gray-500 bg-dark-card rounded-lg border border-gray-800 border-dashed">
No identity providers configured. Add one to enable external authentication.
</div>
)}
</div>
{/* Provider Modal */}
{isModalOpen && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50">
<div className="bg-dark-card rounded-lg border border-gray-800 max-w-3xl w-full p-6 max-h-[90vh] overflow-y-auto">
<h3 className="text-lg font-bold text-white mb-4">
{editingProvider ? 'Edit Provider' : 'Add Provider'}
</h3>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-400 mb-1">Name</label>
<input
type="text"
required
value={formData.name}
onChange={e => setFormData({ ...formData, name: e.target.value })}
placeholder="Google"
className="w-full bg-gray-900 border border-gray-700 rounded-lg px-3 py-2 text-white focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-400 mb-1">Type</label>
<select
value={formData.type}
onChange={e => setFormData({ ...formData, type: e.target.value as 'google' | 'github' | 'oidc' })}
className="w-full bg-gray-900 border border-gray-700 rounded-lg px-3 py-2 text-white focus:ring-2 focus:ring-blue-500"
>
<option value="oidc">Generic OIDC</option>
<option value="google">Google</option>
<option value="github">GitHub</option>
</select>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<div className="flex items-center gap-2 mb-1">
<label className="block text-sm font-medium text-gray-400">Client ID</label>
<HelpTooltip
position="left"
content={
<>
<div className="mb-2">The public identifier for your OAuth application.</div>
<div className="space-y-1">
<div><strong>Google:</strong> <a href="https://console.cloud.google.com/apis/credentials" target="_blank" rel="noopener" className="text-blue-300 hover:text-blue-200 underline">Google Cloud Console</a></div>
<div><strong>GitHub:</strong> <a href="https://github.com/settings/developers" target="_blank" rel="noopener" className="text-blue-300 hover:text-blue-200 underline">Developer Settings</a></div>
</div>
</>
}
/>
</div>
<input
type="text"
required
value={formData.client_id}
onChange={e => setFormData({ ...formData, client_id: e.target.value })}
placeholder="e.g., 123456789.apps.googleusercontent.com"
className="w-full bg-gray-900 border border-gray-700 rounded-lg px-3 py-2 text-white focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<div className="flex items-center gap-2 mb-1">
<label className="block text-sm font-medium text-gray-400">
{editingProvider ? 'Client Secret (leave blank to keep)' : 'Client Secret'}
</label>
<HelpTooltip
position="right"
content={
<>
<div className="mb-2">The private key for your OAuth application. Keep this secret and secure!</div>
<div className="space-y-1">
<div><strong>Google:</strong> <a href="https://console.cloud.google.com/apis/credentials" target="_blank" rel="noopener" className="text-blue-300 hover:text-blue-200 underline">Google Cloud Console</a></div>
<div><strong>GitHub:</strong> <a href="https://github.com/settings/developers" target="_blank" rel="noopener" className="text-blue-300 hover:text-blue-200 underline">Developer Settings</a></div>
</div>
</>
}
/>
</div>
<input
type="password"
required={!editingProvider}
value={formData.client_secret}
onChange={e => setFormData({ ...formData, client_secret: e.target.value })}
placeholder="Enter your client secret"
className="w-full bg-gray-900 border border-gray-700 rounded-lg px-3 py-2 text-white focus:ring-2 focus:ring-blue-500"
/>
</div>
</div>
{formData.type === 'oidc' && (
<div>
<label className="block text-sm font-medium text-gray-400 mb-1">Issuer URL (Discovery)</label>
<input
type="url"
value={formData.issuer_url}
onChange={e => setFormData({ ...formData, issuer_url: e.target.value })}
placeholder="https://accounts.google.com"
className="w-full bg-gray-900 border border-gray-700 rounded-lg px-3 py-2 text-white focus:ring-2 focus:ring-blue-500"
/>
</div>
)}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-400 mb-1">Scopes</label>
<input
type="text"
value={formData.scopes}
onChange={e => setFormData({ ...formData, scopes: e.target.value })}
className="w-full bg-gray-900 border border-gray-700 rounded-lg px-3 py-2 text-white focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-400 mb-1">Display Name</label>
<input
type="text"
value={formData.display_name}
onChange={e => setFormData({ ...formData, display_name: e.target.value })}
placeholder="Sign in with Google"
className="w-full bg-gray-900 border border-gray-700 rounded-lg px-3 py-2 text-white focus:ring-2 focus:ring-blue-500"
/>
</div>
</div>
<div className="flex items-center gap-2">
<input
type="checkbox"
id="enabled"
checked={formData.enabled}
onChange={e => setFormData({ ...formData, enabled: e.target.checked })}
className="w-4 h-4 text-blue-600 bg-gray-900 border-gray-700 rounded focus:ring-blue-500"
/>
<label htmlFor="enabled" className="text-sm text-gray-400">Enabled</label>
</div>
<div className="flex justify-end gap-3 mt-6">
<Button variant="ghost" onClick={() => setIsModalOpen(false)}>Cancel</Button>
<Button type="submit">{editingProvider ? 'Save Changes' : 'Create Provider'}</Button>
</div>
</form>
</div>
</div>
)}
</div>
);
}
-263
View File
@@ -1,263 +0,0 @@
import { useState } from 'react';
import { useAuthUsers } from '../../hooks/useSecurity';
import { Button } from '../../components/ui/Button';
import { Plus, Edit, Trash2, Shield, User } from 'lucide-react';
import toast from 'react-hot-toast';
import type { AuthUser, CreateAuthUserRequest, UpdateAuthUserRequest } from '../../api/security';
export default function Users() {
const { users, createUser, updateUser, deleteUser, isLoading } = useAuthUsers();
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingUser, setEditingUser] = useState<AuthUser | null>(null);
const [formData, setFormData] = useState<CreateAuthUserRequest>({
username: '',
email: '',
name: '',
password: '',
roles: '',
mfa_enabled: false,
additional_emails: '',
});
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
if (editingUser) {
const updateData: UpdateAuthUserRequest = {
email: formData.email,
name: formData.name,
roles: formData.roles,
mfa_enabled: formData.mfa_enabled,
additional_emails: formData.additional_emails,
};
if (formData.password) {
updateData.password = formData.password;
}
await updateUser({ uuid: editingUser.uuid, data: updateData });
toast.success('User updated successfully');
} else {
await createUser(formData);
toast.success('User created successfully');
}
setIsModalOpen(false);
resetForm();
} catch (error: unknown) {
const err = error as { response?: { data?: { error?: string } } };
toast.error(err.response?.data?.error || 'Failed to save user');
}
};
const handleDelete = async (uuid: string) => {
if (confirm('Are you sure you want to delete this user?')) {
try {
await deleteUser(uuid);
toast.success('User deleted successfully');
} catch (error: unknown) {
const err = error as { response?: { data?: { error?: string } } };
toast.error(err.response?.data?.error || 'Failed to delete user');
}
}
};
const resetForm = () => {
setFormData({
username: '',
email: '',
name: '',
password: '',
roles: '',
mfa_enabled: false,
additional_emails: '',
});
setEditingUser(null);
};
const openEditModal = (user: AuthUser) => {
setEditingUser(user);
setFormData({
username: user.username,
email: user.email,
name: user.name,
password: '', // Don't populate password
roles: user.roles,
mfa_enabled: user.mfa_enabled,
additional_emails: user.additional_emails || '',
});
setIsModalOpen(true);
};
if (isLoading) return <div>Loading...</div>;
return (
<div className="space-y-6">
<div className="flex justify-between items-center">
<h2 className="text-xl font-semibold text-white">Local Users</h2>
<Button onClick={() => { resetForm(); setIsModalOpen(true); }}>
<Plus size={16} className="mr-2" />
Add User
</Button>
</div>
<div className="bg-dark-card rounded-lg border border-gray-800 overflow-hidden">
<table className="w-full text-left text-sm text-gray-400">
<thead className="bg-gray-900 text-gray-200 uppercase font-medium">
<tr>
<th className="px-6 py-3">User</th>
<th className="px-6 py-3">Name</th>
<th className="px-6 py-3">Roles</th>
<th className="px-6 py-3">Created</th>
<th className="px-6 py-3 text-right">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-800">
{users.map((user) => (
<tr key={user.uuid} className="hover:bg-gray-800/50 transition-colors">
<td className="px-6 py-4">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-blue-900/30 flex items-center justify-center text-blue-400">
<User size={16} />
</div>
<div>
<div className="font-medium text-white">{user.username}</div>
<div className="text-xs text-gray-500">{user.email}</div>
</div>
</div>
</td>
<td className="px-6 py-4">
{user.name}
</td>
<td className="px-6 py-4">
{user.roles ? (
<span className="text-blue-400 flex items-center gap-1">
<Shield size={14} /> {user.roles}
</span>
) : (
<span className="text-gray-600">User</span>
)}
</td>
<td className="px-6 py-4">
{new Date(user.created_at).toLocaleDateString()}
</td>
<td className="px-6 py-4 text-right">
<div className="flex justify-end gap-2">
<button
onClick={() => openEditModal(user)}
className="p-1.5 rounded-md hover:bg-gray-700 text-gray-400 hover:text-white transition-colors"
>
<Edit size={16} />
</button>
<button
onClick={() => handleDelete(user.uuid)}
className="p-1.5 rounded-md hover:bg-red-900/30 text-gray-400 hover:text-red-400 transition-colors"
>
<Trash2 size={16} />
</button>
</div>
</td>
</tr>
))}
{users.length === 0 && (
<tr>
<td colSpan={5} className="px-6 py-8 text-center text-gray-500">
No users found. Create one to get started.
</td>
</tr>
)}
</tbody>
</table>
</div>
{/* User Modal */}
{isModalOpen && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50">
<div className="bg-dark-card rounded-lg border border-gray-800 max-w-md w-full p-6">
<h3 className="text-lg font-bold text-white mb-4">
{editingUser ? 'Edit User' : 'Add User'}
</h3>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-400 mb-1">Username</label>
<input
type="text"
required
value={formData.username}
onChange={e => setFormData({ ...formData, username: e.target.value })}
className="w-full bg-gray-900 border border-gray-700 rounded-lg px-3 py-2 text-white focus:ring-2 focus:ring-blue-500"
disabled={!!editingUser}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-400 mb-1">Email</label>
<input
type="email"
required
value={formData.email}
onChange={e => setFormData({ ...formData, email: e.target.value })}
className="w-full bg-gray-900 border border-gray-700 rounded-lg px-3 py-2 text-white focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-400 mb-1">Full Name</label>
<input
type="text"
required
value={formData.name}
onChange={e => setFormData({ ...formData, name: e.target.value })}
className="w-full bg-gray-900 border border-gray-700 rounded-lg px-3 py-2 text-white focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-400 mb-1">
{editingUser ? 'New Password (leave blank to keep)' : 'Password'}
</label>
<input
type="password"
required={!editingUser}
value={formData.password}
onChange={e => setFormData({ ...formData, password: e.target.value })}
className="w-full bg-gray-900 border border-gray-700 rounded-lg px-3 py-2 text-white focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-400 mb-1">Roles (comma separated)</label>
<input
type="text"
value={formData.roles}
onChange={e => setFormData({ ...formData, roles: e.target.value })}
placeholder="admin, editor"
className="w-full bg-gray-900 border border-gray-700 rounded-lg px-3 py-2 text-white focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-400 mb-1">Additional Emails (comma separated)</label>
<input
type="text"
value={formData.additional_emails || ''}
onChange={e => setFormData({ ...formData, additional_emails: e.target.value })}
placeholder="email2@example.com, email3@example.com"
className="w-full bg-gray-900 border border-gray-700 rounded-lg px-3 py-2 text-white focus:ring-2 focus:ring-blue-500"
/>
<p className="text-xs text-gray-500 mt-1">Used for linking multiple OAuth identities to this user.</p>
</div>
<div className="flex items-center gap-2">
<input
type="checkbox"
id="mfa_enabled"
checked={formData.mfa_enabled}
onChange={e => setFormData({ ...formData, mfa_enabled: e.target.checked })}
className="w-4 h-4 text-blue-600 bg-gray-900 border-gray-700 rounded focus:ring-blue-500"
/>
<label htmlFor="mfa_enabled" className="text-sm text-gray-400">MFA Enabled</label>
</div>
<div className="flex justify-end gap-3 mt-6">
<Button variant="ghost" onClick={() => setIsModalOpen(false)}>Cancel</Button>
<Button type="submit">{editingUser ? 'Save Changes' : 'Create User'}</Button>
</div>
</form>
</div>
</div>
)}
</div>
);
}
-62
View File
@@ -1,62 +0,0 @@
import { useState } from 'react';
import { Users, Globe, Lock } from 'lucide-react';
import UsersPage from './Users';
import ProvidersPage from './Providers';
import PoliciesPage from './Policies';
export default function Security() {
const [activeTab, setActiveTab] = useState<'users' | 'providers' | 'policies'>('users');
return (
<div className="space-y-6">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<h1 className="text-2xl font-bold text-white">Security & Access Control</h1>
<p className="text-gray-400">Manage users, identity providers, and access policies for your services.</p>
</div>
</div>
<div className="flex border-b border-gray-800">
<button
onClick={() => setActiveTab('users')}
className={`px-6 py-3 text-sm font-medium border-b-2 transition-colors flex items-center gap-2 ${
activeTab === 'users'
? 'border-blue-500 text-blue-500'
: 'border-transparent text-gray-400 hover:text-gray-300 hover:border-gray-700'
}`}
>
<Users size={16} />
Users
</button>
<button
onClick={() => setActiveTab('providers')}
className={`px-6 py-3 text-sm font-medium border-b-2 transition-colors flex items-center gap-2 ${
activeTab === 'providers'
? 'border-blue-500 text-blue-500'
: 'border-transparent text-gray-400 hover:text-gray-300 hover:border-gray-700'
}`}
>
<Globe size={16} />
Identity Providers
</button>
<button
onClick={() => setActiveTab('policies')}
className={`px-6 py-3 text-sm font-medium border-b-2 transition-colors flex items-center gap-2 ${
activeTab === 'policies'
? 'border-blue-500 text-blue-500'
: 'border-transparent text-gray-400 hover:text-gray-300 hover:border-gray-700'
}`}
>
<Lock size={16} />
Access Policies
</button>
</div>
<div className="pt-4">
{activeTab === 'users' && <UsersPage />}
{activeTab === 'providers' && <ProvidersPage />}
{activeTab === 'policies' && <PoliciesPage />}
</div>
</div>
);
}
-4
View File
@@ -15,8 +15,6 @@ export const mockProxyHosts: ProxyHost[] = [
hsts_subdomains: false,
block_exploits: true,
websocket_support: true,
forward_auth_enabled: false,
forward_auth_bypass: '',
locations: [],
advanced_config: undefined,
enabled: true,
@@ -36,8 +34,6 @@ export const mockProxyHosts: ProxyHost[] = [
hsts_subdomains: false,
block_exploits: true,
websocket_support: false,
forward_auth_enabled: false,
forward_auth_bypass: '',
locations: [],
advanced_config: undefined,
enabled: true,