add auth
This commit is contained in:
100
frontend/src/pages/LoginPage.tsx
Normal file
100
frontend/src/pages/LoginPage.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import { useState, type FormEvent } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useAuth } from "../auth/useAuth";
|
||||
|
||||
export default function LoginPage() {
|
||||
const { login, isAuthenticated } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
if (isAuthenticated) {
|
||||
navigate("/", { replace: true });
|
||||
return null;
|
||||
}
|
||||
|
||||
async function handleSubmit(e: FormEvent) {
|
||||
e.preventDefault();
|
||||
setError("");
|
||||
setLoading(true);
|
||||
try {
|
||||
await login(username, password);
|
||||
navigate("/", { replace: true });
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Login failed");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
height: "100vh",
|
||||
background: "var(--bg)",
|
||||
}}>
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
style={{
|
||||
width: 320,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 16,
|
||||
padding: 32,
|
||||
borderRadius: 8,
|
||||
background: "var(--bg-secondary)",
|
||||
border: "1px solid var(--border)",
|
||||
}}
|
||||
>
|
||||
<h1 style={{ margin: 0, fontSize: 24, color: "var(--text)", textAlign: "center" }}>
|
||||
MediaLore
|
||||
</h1>
|
||||
|
||||
<label style={{ display: "flex", flexDirection: "column", gap: 4 }}>
|
||||
<span style={{ fontSize: 13, color: "var(--text-secondary)" }}>Username</span>
|
||||
<input
|
||||
type="text"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
required
|
||||
autoFocus
|
||||
autoComplete="username"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label style={{ display: "flex", flexDirection: "column", gap: 4 }}>
|
||||
<span style={{ fontSize: 13, color: "var(--text-secondary)" }}>Password</span>
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
</label>
|
||||
|
||||
{error && (
|
||||
<p style={{ color: "var(--danger)", margin: 0, fontSize: 13 }}>{error}</p>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
style={{
|
||||
background: "var(--accent)",
|
||||
color: "#fff",
|
||||
border: "none",
|
||||
padding: "10px 0",
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{loading ? "Signing in…" : "Sign in"}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user