import { useState } from "react"; import { useTranslation } from "react-i18next"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { ApiError, can, createUser, deleteUser, fetchRoles, fetchUsers, resetUserPassword, updateUser, type ManagedRole, type ManagedUser, type SessionUser, } from "./api.js"; import { Modal } from "./ui/Modal.js"; // User management (admin). List users, create one (username + password + role), // change a user's role, reset a password, delete. The server enforces the same // permissions and the no-lockout rule (the last admin can't be removed). See // wiki/entities/local-jwt-auth.md. export function UsersManager({ user }: { user: SessionUser | null }) { const { t } = useTranslation(); const qc = useQueryClient(); const usersQ = useQuery({ queryKey: ["users"], queryFn: fetchUsers }); const rolesQ = useQuery({ queryKey: ["roles"], queryFn: fetchRoles }); const canCreate = can(user, "user:create"); const canUpdate = can(user, "user:update"); const canDelete = can(user, "user:delete"); const roles: ManagedRole[] = rolesQ.data?.roles ?? []; const [error, setError] = useState(null); const [adding, setAdding] = useState(false); const [editingUser, setEditingUser] = useState(null); const invalidate = () => void qc.invalidateQueries({ queryKey: ["users"] }); const onError = (e: unknown) => setError(e instanceof ApiError ? e.message : (e as Error).message); return (

{t("users.title")}

{canCreate && roles.length > 0 && ( )}
{error &&
{error}
} setAdding(false)} title={t("users.new")} width="max-w-2xl"> setAdding(false)} onSubmit={async (v) => { try { await createUser({ username: v.username, password: v.password!, roleId: v.roleId, fullName: v.fullName, phone: v.phone, email: v.email, address: v.address, }); setAdding(false); invalidate(); } catch (e) { onError(e); } }} /> setEditingUser(null)} title={t("users.editTitle")} width="max-w-2xl"> {editingUser && ( setEditingUser(null)} onSubmit={async (v) => { try { await updateUser(editingUser.id, { username: v.username, roleId: v.roleId, fullName: v.fullName, phone: v.phone, email: v.email, address: v.address, }); setEditingUser(null); invalidate(); } catch (e) { onError(e); } }} /> )}
{(usersQ.data?.users ?? []).map((u) => ( { setEditingUser(u); setError(null); }} onChanged={invalidate} onError={onError} /> ))} {usersQ.data?.users.length === 0 && ( )}
{t("users.username")} {t("users.role")} {t("common.none")}
{t("users.none")}
); } function UserRow({ u, roles, canUpdate, canDelete, onEdit, onChanged, onError, }: { u: ManagedUser; roles: ManagedRole[]; canUpdate: boolean; canDelete: boolean; onEdit: () => void; onChanged: () => void; onError: (e: unknown) => void; }) { const { t } = useTranslation(); const [resetting, setResetting] = useState(false); const [pw, setPw] = useState(""); const roleMut = useMutation({ mutationFn: (roleId: string) => updateUser(u.id, { roleId }), onSuccess: onChanged, onError, }); const pwMut = useMutation({ mutationFn: () => resetUserPassword(u.id, pw), onSuccess: () => { setResetting(false); setPw(""); }, onError, }); const delMut = useMutation({ mutationFn: () => deleteUser(u.id), onSuccess: onChanged, onError, }); return ( {u.username} {u.fullName && {u.fullName}} {canUpdate ? ( ) : ( u.roleName )}
{canUpdate && !resetting && ( )} {canUpdate && !resetting && ( )} {canUpdate && resetting && ( setPw(e.target.value)} placeholder={t("users.newPassword")} className="input input-sm w-32" /> )} {canDelete && ( )}
); } /** Submitted form value. `password` is omitted entirely on edit (a blank field must * not blank the password — that's the separate "reset password" flow). */ interface UserFormValue { username: string; password?: string; roleId: string; fullName: string; phone: string; email: string; address: string; } function UserForm({ roles, editing, onCancel, onSubmit, }: { roles: ManagedRole[]; /** When set, the form edits this user (username/role/details; NOT the password). */ editing?: ManagedUser; onCancel: () => void; onSubmit: (v: UserFormValue) => void; }) { const { t } = useTranslation(); const isEdit = editing != null; const [username, setUsername] = useState(editing?.username ?? ""); const [password, setPassword] = useState(""); const [roleId, setRoleId] = useState(editing?.roleId ?? roles[0]?.id ?? ""); const [fullName, setFullName] = useState(editing?.fullName ?? ""); const [phone, setPhone] = useState(editing?.phone ?? ""); const [email, setEmail] = useState(editing?.email ?? ""); const [address, setAddress] = useState(editing?.address ?? ""); // On create, a >=8 char password is required; on edit it's left untouched. const valid = username.trim().length > 0 && roleId && (isEdit || password.length >= 8); function submit() { onSubmit({ username: username.trim(), ...(isEdit ? {} : { password }), roleId, fullName, phone, email, address, }); } return (
{t("users.username")} setUsername(e.target.value)} />
{!isEdit && (
{t("users.password")} setPassword(e.target.value)} />
)}
{t("users.role")}
{!isEdit &&
{t("users.passwordHint")}
} {/* Optional profile metadata. */}
{t("users.detailsSection")}
{t("users.fullName")} setFullName(e.target.value)} />
{t("users.phone")} setPhone(e.target.value)} />
{t("users.email")} setEmail(e.target.value)} />
{t("users.address")} setAddress(e.target.value)} />
); }