This commit is contained in:
Garret Patti
2026-06-29 10:41:29 -04:00
parent d48c1e973e
commit 2b0b19eb91
23 changed files with 969 additions and 44 deletions

View File

@@ -1,6 +1,7 @@
import { useState } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { api, type Library } from "../api/client";
import { api, type Library, type AuthUser } from "../api/client";
import { useAuth } from "../auth/useAuth";
function LibraryRow({ lib, onRemove }: { lib: Library; onRemove: (id: number) => void }) {
const qc = useQueryClient();
@@ -48,6 +49,91 @@ function LibraryRow({ lib, onRemove }: { lib: Library; onRemove: (id: number) =>
);
}
function UserManagement() {
const qc = useQueryClient();
const { user: currentUser } = useAuth();
const { data: users = [] } = useQuery<AuthUser[]>({
queryKey: ["users"],
queryFn: api.auth.listUsers,
});
const [newUsername, setNewUsername] = useState("");
const [newPassword, setNewPassword] = useState("");
const [userError, setUserError] = useState("");
const createMutation = useMutation({
mutationFn: () => api.auth.createUser(newUsername, newPassword),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ["users"] });
setNewUsername("");
setNewPassword("");
setUserError("");
},
onError: (e: Error) => setUserError(e.message),
});
const deleteMutation = useMutation({
mutationFn: (id: number) => api.auth.deleteUser(id),
onSuccess: () => qc.invalidateQueries({ queryKey: ["users"] }),
onError: (e: Error) => setUserError(e.message),
});
return (
<div>
<h2 style={{ color: "var(--text)", marginTop: 32 }}>Users</h2>
<form
onSubmit={(e) => { e.preventDefault(); createMutation.mutate(); }}
style={{ display: "flex", flexDirection: "column", gap: 8, marginBottom: 24 }}
>
<input
placeholder="Username"
value={newUsername}
onChange={(e) => setNewUsername(e.target.value)}
required
autoComplete="off"
/>
<input
type="password"
placeholder="Password"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
required
autoComplete="new-password"
/>
{userError && <p style={{ color: "var(--danger)", margin: 0, fontSize: 13 }}>{userError}</p>}
<button type="submit" disabled={createMutation.isPending} style={{ background: "var(--accent)", color: "#fff", border: "none" }}>
{createMutation.isPending ? "Creating…" : "Add User"}
</button>
</form>
<ul style={{ listStyle: "none", padding: 0 }}>
{users.map((u) => (
<li key={u.id} style={{ display: "flex", justifyContent: "space-between", alignItems: "center", padding: "8px 0", borderBottom: "1px solid var(--border-subtle)" }}>
<div>
<strong style={{ color: "var(--text)" }}>{u.username}</strong>
{u.is_admin && (
<span style={{ marginLeft: 8, fontSize: 11, color: "var(--accent)", fontWeight: 600 }}>
ADMIN
</span>
)}
</div>
{u.id !== currentUser?.id && (
<button
onClick={() => deleteMutation.mutate(u.id)}
disabled={deleteMutation.isPending}
style={{ color: "var(--danger)", background: "transparent", border: "none" }}
>
Remove
</button>
)}
</li>
))}
</ul>
</div>
);
}
export default function SettingsPage() {
const qc = useQueryClient();
const { data: libraries = [] } = useQuery<Library[]>({
@@ -97,6 +183,8 @@ export default function SettingsPage() {
<LibraryRow key={lib.id} lib={lib} onRemove={(id) => deleteMutation.mutate(id)} />
))}
</ul>
<UserManagement />
</div>
);
}