/** * User management page (admin only): user list + create / reset password / delete. * Registration is closed: new users are created here, with the initial password set by the admin and * communicated offline; deleting a user also deletes all their Projects (including data directories), * with a confirmation dialog. */ import { useCallback, useEffect, useState } from "react"; import { Navigate } from "react-router"; import type { UserInfo } from "@prismshadow/penguin-server/api"; import * as api from "../../api/endpoints"; import { ApiError } from "../../api/client"; import { S } from "../../lib/strings"; import { USERNAME_PATTERN } from "../../lib/semantic-id"; import { formatDateTime } from "../../lib/format"; import { useDocumentTitle } from "../../lib/use-document-title"; import { useAuth } from "../../state/auth"; import { Badge } from "../../components/ui/badge"; import { Button } from "../../components/ui/button"; import { Input } from "../../components/ui/input"; import { PasswordInput } from "../../components/ui/password-input"; import { Modal } from "../../components/ui/modal"; export function AdminUsersPage() { useDocumentTitle(S.admin.users); const { user } = useAuth(); const [users, setUsers] = useState(null); const [listError, setListError] = useState(null); const [createOpen, setCreateOpen] = useState(false); const [resetting, setResetting] = useState(null); const [deleting, setDeleting] = useState(null); const reload = useCallback(async () => { try { setUsers((await api.adminListUsers()).users); setListError(null); } catch (e) { setListError(e instanceof ApiError ? e.message : S.common.unknownError); } }, []); useEffect(() => { if (user?.isAdmin) void reload(); }, [user?.isAdmin, reload]); // Route guard fallback: non-admins are redirected back to the chat page (the sidebar has no entry point anyway). if (user && !user.isAdmin) return ; return (

{S.admin.users}

{users === null ? (

{S.common.loading}

) : (
{users.map((u) => ( ))}
{S.admin.userId} {S.admin.role} {S.admin.createdAt} {S.admin.actions}
{u.userId} {u.passwordIsInitial && ( {S.admin.initialPasswordFlag} )} {u.isAdmin ? S.admin.roleAdmin : S.admin.roleUser} {formatDateTime(u.createdAt)} {!u.isAdmin && ( )}
)} {listError &&

{listError}

}
setCreateOpen(false)} onDone={() => { setCreateOpen(false); void reload(); }} /> setResetting(null)} /> setDeleting(null)} onDone={() => { setDeleting(null); void reload(); }} />
); } function CreateUserDialog({ open, onClose, onDone, }: { open: boolean; onClose: () => void; onDone: () => void; }) { const [userId, setUserId] = useState(""); const [password, setPassword] = useState(""); const [error, setError] = useState(null); const [busy, setBusy] = useState(false); useEffect(() => { if (!open) return; setUserId(""); setPassword(""); setError(null); }, [open]); const submit = async () => { const id = userId.trim(); if (!id || !password) { setError(S.common.requiredField); return; } if (!USERNAME_PATTERN.test(id)) { setError(S.auth.usernameHint); return; } setBusy(true); setError(null); try { await api.adminCreateUser({ userId: id, password }); onDone(); } catch (e) { setError(e instanceof ApiError ? e.message : S.common.unknownError); } finally { setBusy(false); } }; return ( } >
setUserId(e.target.value)} hint={S.auth.usernameHint} autoFocus /> setPassword(e.target.value)} autoComplete="new-password" hint={S.auth.passwordHint} /> {USERNAME_PATTERN.test(userId.trim()) && (

{S.admin.defaultProjectNote(`${userId.trim()}-default_project`)}

)} {error &&

{error}

}
); } function ResetPasswordDialog({ user, onClose }: { user: UserInfo | null; onClose: () => void }) { const [password, setPassword] = useState(""); const [error, setError] = useState(null); const [busy, setBusy] = useState(false); useEffect(() => { if (!user) return; setPassword(""); setError(null); }, [user]); const submit = async () => { if (!user) return; if (!password) { setError(S.common.requiredField); return; } setBusy(true); setError(null); try { await api.adminResetPassword(user.userId, { password }); onClose(); } catch (e) { setError(e instanceof ApiError ? e.message : S.common.unknownError); } finally { setBusy(false); } }; return ( } >
setPassword(e.target.value)} autoComplete="new-password" hint={S.auth.passwordHint} autoFocus />

{S.admin.resetPasswordNote}

{error &&

{error}

}
); } function DeleteUserDialog({ user, onClose, onDone, }: { user: UserInfo | null; onClose: () => void; onDone: () => void; }) { const [confirmed, setConfirmed] = useState(false); const [error, setError] = useState(null); const [busy, setBusy] = useState(false); useEffect(() => { if (!user) return; setConfirmed(false); setError(null); }, [user]); const doDelete = async () => { if (!user) return; setBusy(true); setError(null); try { await api.adminDeleteUser(user.userId); onDone(); } catch (e) { setError(e instanceof ApiError ? e.message : S.common.unknownError); } finally { setBusy(false); } }; return ( {confirmed ? ( ) : ( )} } >

{user ? S.admin.deleteUserConfirm(user.userId) : ""}

{error &&

{error}

}
); }