From 1e9a6271ea8d7c6b6bb481123316d5fae8f40a9e Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Sat, 27 Dec 2025 18:47:43 +0000 Subject: [PATCH] feat: add user management subcomponents --- .../managers/user-management/AuditTrail.tsx | 149 ++++++++++++++++++ .../managers/user-management/RoleEditor.tsx | 125 +++++++++++++++ .../managers/user-management/UserList.tsx | 149 ++++++++++++++++++ 3 files changed, 423 insertions(+) create mode 100644 frontends/nextjs/src/components/managers/user-management/AuditTrail.tsx create mode 100644 frontends/nextjs/src/components/managers/user-management/RoleEditor.tsx create mode 100644 frontends/nextjs/src/components/managers/user-management/UserList.tsx diff --git a/frontends/nextjs/src/components/managers/user-management/AuditTrail.tsx b/frontends/nextjs/src/components/managers/user-management/AuditTrail.tsx new file mode 100644 index 000000000..216ebf7c9 --- /dev/null +++ b/frontends/nextjs/src/components/managers/user-management/AuditTrail.tsx @@ -0,0 +1,149 @@ +'use client' +import { useMemo, useState } from 'react' +import { + Badge, + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, + Input, + Label, + ScrollArea, + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui' +import { Clock, ShieldWarning, UserSwitch } from '@phosphor-icons/react' +export type AuditSeverity = 'info' | 'warning' | 'critical' +export interface AuditEvent { + id: string + actor: string + action: string + target?: string + timestamp: string | number + severity: AuditSeverity +} +interface AuditTrailProps { + events: AuditEvent[] + showSearch?: boolean + maxRows?: number +} +const SEVERITY_META: Record = { + info: { label: 'Info', variant: 'secondary' }, + warning: { label: 'Warning', variant: 'default' }, + critical: { label: 'Critical', variant: 'destructive' }, +} +const formatTime = (value: string | number) => new Date(value).toLocaleString() +export function AuditTrail({ events, showSearch = true, maxRows }: AuditTrailProps) { + const [query, setQuery] = useState('') + const [severity, setSeverity] = useState('all') + const filtered = useMemo(() => { + const text = query.toLowerCase() + return events + .filter((event) => { + const matchesText = + !text || `${event.actor} ${event.action} ${event.target ?? ''}`.toLowerCase().includes(text) + const matchesSeverity = severity === 'all' || event.severity === severity + return matchesText && matchesSeverity + }) + .slice(0, maxRows ?? events.length) + }, [events, query, severity, maxRows]) + + return ( + + +
+
+ Audit trail + Recent security-sensitive actions. +
+ + + {events.length} events + +
+ {showSearch && ( +
+
+ + setQuery(event.target.value)} + /> +
+
+ + +
+
+ )} +
+ + + + + + Timestamp + Actor + Action + Severity + + + + {filtered.map((event) => ( + + +
+ + {formatTime(event.timestamp)} +
+
+ {event.actor} + + {event.action} + {event.target && ( + + {event.target} + + )} + + + + + {SEVERITY_META[event.severity].label} + + +
+ ))} + {filtered.length === 0 && ( + + + No audit events found. + + + )} +
+
+
+
+
+ ) +} diff --git a/frontends/nextjs/src/components/managers/user-management/RoleEditor.tsx b/frontends/nextjs/src/components/managers/user-management/RoleEditor.tsx new file mode 100644 index 000000000..15acb1179 --- /dev/null +++ b/frontends/nextjs/src/components/managers/user-management/RoleEditor.tsx @@ -0,0 +1,125 @@ +'use client' +import { + Badge, + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, + Label, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, + Switch, +} from '@/components/ui' +import type { UserRole } from '@/lib/level-types' + +interface RoleEditorProps { + role: UserRole + onRoleChange: (role: UserRole) => void + isInstanceOwner?: boolean + onInstanceOwnerChange?: (value: boolean) => void + allowedRoles?: UserRole[] +} + +const ROLE_INFO: Record = { + public: { + blurb: 'Read-only access for guest viewers.', + highlights: ['View public resources', 'No authentication needed'], + }, + user: { + blurb: 'Standard workspace member with personal settings.', + highlights: ['Create content', 'Access shared libraries'], + }, + moderator: { + blurb: 'Content moderator with collaboration tools.', + highlights: ['Manage comments', 'Resolve reports', 'Escalate to admins'], + }, + admin: { + blurb: 'Tenant-level administrator controls.', + highlights: ['Invite users', 'Configure pages', 'Reset credentials'], + }, + god: { + blurb: 'Power user with platform configuration access.', + highlights: ['Manage integrations', 'Run advanced scripts', 'Override safety flags'], + }, + supergod: { + blurb: 'Instance owner with full control.', + highlights: ['Edit system settings', 'Manage tenants', 'Bypass feature gates'], + }, +} + +const roleLabel = (role: UserRole) => role.charAt(0).toUpperCase() + role.slice(1) + +export function RoleEditor({ + role, + onRoleChange, + isInstanceOwner, + onInstanceOwnerChange, + allowedRoles, +}: RoleEditorProps) { + const options = allowedRoles ?? (Object.keys(ROLE_INFO) as UserRole[]) + + return ( + + + User role + Adjust access level and ownership flags. + + +
+ + +
+ +
+
+
+

{roleLabel(role)}

+

{ROLE_INFO[role].blurb}

+
+ {ROLE_INFO[role].highlights.length} capabilities +
+
    + {ROLE_INFO[role].highlights.map((item) => ( +
  • + + {item} +
  • + ))} +
+
+ + {onInstanceOwnerChange && ( +
+
+

Instance owner

+

+ Grants access to backup, billing, and infrastructure settings. +

+
+ +
+ )} +
+
+ ) +} diff --git a/frontends/nextjs/src/components/managers/user-management/UserList.tsx b/frontends/nextjs/src/components/managers/user-management/UserList.tsx new file mode 100644 index 000000000..5dd9151c5 --- /dev/null +++ b/frontends/nextjs/src/components/managers/user-management/UserList.tsx @@ -0,0 +1,149 @@ +'use client' +import { useMemo, useState } from 'react' +import { + Avatar, + AvatarFallback, + Badge, + Button, + Input, + Label, + ScrollArea, + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui' +import { FunnelSimple, PencilSimple, Trash } from '@phosphor-icons/react' +import type { User, UserRole } from '@/lib/level-types' +interface UserListProps { + users: User[] + onEdit?: (user: User) => void + onDelete?: (user: User) => void + compact?: boolean +} +const ROLE_STYLES: Record = { + public: { label: 'Public', variant: 'outline' }, + user: { label: 'User', variant: 'outline' }, + moderator: { label: 'Moderator', variant: 'secondary' }, + admin: { label: 'Admin', variant: 'secondary' }, + god: { label: 'God', variant: 'default' }, + supergod: { label: 'Supergod', variant: 'default' }, + } + +function initials(value: string) { + return value + .split(' ') + .map((chunk) => chunk[0]) + .join('') + .slice(0, 2) + .toUpperCase() +} + +export function UserList({ users, onEdit, onDelete, compact }: UserListProps) { + const [query, setQuery] = useState('') + const [role, setRole] = useState('all') + + const filtered = useMemo(() => { + return users.filter((user) => { + const matchesQuery = `${user.username} ${user.email}` + .toLowerCase() + .includes(query.toLowerCase()) + const matchesRole = role === 'all' || user.role === role + return matchesQuery && matchesRole + }) + }, [users, query, role]) + + return ( +
+
+
+ + setQuery(event.target.value)} + /> +
+
+ + +
+
+ + + + + + User + Email + Role + Joined + {(onEdit || onDelete) && Actions} + + + + {filtered.map((user) => ( + + + + {user.profilePicture && {user.username}} + {initials(user.username)} + +
+
{user.username}
+
ID: {user.id}
+
+
+ {user.email} + + {ROLE_STYLES[user.role]?.label} + + + {new Date(user.createdAt).toLocaleDateString()} + + {(onEdit || onDelete) && ( + + {onEdit && ( + + )} + {onDelete && ( + + )} + + )} +
+ ))} + {filtered.length === 0 && ( + + + No users match your filters. + + + )} +
+
+
+
+ ) +}