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:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user