Add admin password change form in Settings.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jos Vooges | STH 2026-08-29 02:19:33 +02:00
parent d53a4f9297
commit 2489e9b1fc
2 changed files with 142 additions and 0 deletions

View file

@ -63,6 +63,13 @@ export default function SettingsPage() {
const [shelves, setShelves] = useState<ShelfRow[]>([]); const [shelves, setShelves] = useState<ShelfRow[]>([]);
const [newShelfName, setNewShelfName] = useState(""); const [newShelfName, setNewShelfName] = useState("");
const [newShelfKind, setNewShelfKind] = useState<"MOVIE" | "SERIES">("MOVIE"); const [newShelfKind, setNewShelfKind] = useState<"MOVIE" | "SERIES">("MOVIE");
const [adminEmail, setAdminEmail] = useState("");
const [currentPassword, setCurrentPassword] = useState("");
const [newPassword, setNewPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [pwBusy, setPwBusy] = useState(false);
const [pwMsg, setPwMsg] = useState<string | null>(null);
const [pwErr, setPwErr] = useState<string | null>(null);
const loadTokens = useCallback(() => { const loadTokens = useCallback(() => {
fetch("/api/v1/admin/addon-tokens", { credentials: "include" }) fetch("/api/v1/admin/addon-tokens", { credentials: "include" })
@ -79,8 +86,49 @@ export default function SettingsPage() {
useEffect(() => { useEffect(() => {
loadTokens(); loadTokens();
loadShelves(); loadShelves();
fetch("/api/v1/auth/me", { credentials: "include" })
.then((r) => r.json())
.then((d) => setAdminEmail(d.user?.email ?? ""))
.catch(() => undefined);
}, [loadTokens, loadShelves]); }, [loadTokens, loadShelves]);
async function changePassword() {
setPwMsg(null);
setPwErr(null);
if (!currentPassword || !newPassword) {
setPwErr("Vul huidig en nieuw wachtwoord in");
return;
}
if (newPassword.length < 8) {
setPwErr("Nieuw wachtwoord moet minstens 8 tekens zijn");
return;
}
if (newPassword !== confirmPassword) {
setPwErr("Nieuwe wachtwoorden komen niet overeen");
return;
}
setPwBusy(true);
try {
const res = await fetch("/api/v1/auth/change-password", {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ currentPassword, newPassword, confirmPassword }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
setPwErr(data.error?.message ?? "Wachtwoord wijzigen mislukt");
return;
}
setCurrentPassword("");
setNewPassword("");
setConfirmPassword("");
setPwMsg("Wachtwoord gewijzigd. Andere sessies zijn uitgelogd.");
} finally {
setPwBusy(false);
}
}
async function createShelf() { async function createShelf() {
if (!newShelfName.trim()) return; if (!newShelfName.trim()) return;
const res = await fetch("/api/v1/admin/library/shelves", { const res = await fetch("/api/v1/admin/library/shelves", {
@ -288,6 +336,48 @@ export default function SettingsPage() {
</div> </div>
</div> </div>
<div className="card" style={{ marginBottom: "2rem" }}>
<h2 style={{ fontSize: "1rem", color: "#fafafa", textTransform: "none", marginBottom: "0.5rem" }}>
Account
</h2>
<p className="muted" style={{ marginBottom: "1rem" }}>
Ingelogd als <strong style={{ color: "var(--text-bright)" }}>{adminEmail || "…"}</strong>.
Wijzig hier je admin-wachtwoord (min. 8 tekens).
</p>
{pwErr && <div className="error">{pwErr}</div>}
{pwMsg && <p className="muted" style={{ color: "var(--ok)", marginBottom: "0.75rem" }}>{pwMsg}</p>}
<div className="form-group">
<label>Huidig wachtwoord</label>
<input
type="password"
value={currentPassword}
onChange={(e) => setCurrentPassword(e.target.value)}
autoComplete="current-password"
/>
</div>
<div className="form-group">
<label>Nieuw wachtwoord</label>
<input
type="password"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
autoComplete="new-password"
/>
</div>
<div className="form-group">
<label>Bevestig nieuw wachtwoord</label>
<input
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
autoComplete="new-password"
/>
</div>
<button type="button" onClick={changePassword} disabled={pwBusy}>
{pwBusy ? "Bezig…" : "Wachtwoord wijzigen"}
</button>
</div>
<div className="card" style={{ marginBottom: "2rem" }}> <div className="card" style={{ marginBottom: "2rem" }}>
<h2 style={{ fontSize: "1rem", color: "#fafafa", textTransform: "none", marginBottom: "0.5rem" }}> <h2 style={{ fontSize: "1rem", color: "#fafafa", textTransform: "none", marginBottom: "0.5rem" }}>
Bibliotheek-planken (Stremio) Bibliotheek-planken (Stremio)

View file

@ -69,6 +69,58 @@ export async function registerAuthRoutes(
const user = (request as AuthenticatedRequest).user; const user = (request as AuthenticatedRequest).user;
return { user: { id: user.id, email: user.email, name: user.name } }; return { user: { id: user.id, email: user.email, name: user.name } };
}); });
app.post("/api/v1/auth/change-password", { preHandler: requireAdmin }, async (request, reply) => {
const authUser = (request as AuthenticatedRequest).user;
const body = request.body as {
currentPassword?: string;
newPassword?: string;
confirmPassword?: string;
};
const currentPassword = body.currentPassword ?? "";
const newPassword = body.newPassword ?? "";
const confirmPassword = body.confirmPassword ?? "";
if (!currentPassword || !newPassword) {
throw new AppError("INVALID_REQUEST", "Huidig en nieuw wachtwoord zijn verplicht", 400);
}
if (newPassword.length < 8) {
throw new AppError("INVALID_REQUEST", "Nieuw wachtwoord moet minstens 8 tekens zijn", 400);
}
if (newPassword !== confirmPassword) {
throw new AppError("INVALID_REQUEST", "Nieuwe wachtwoorden komen niet overeen", 400);
}
if (newPassword === currentPassword) {
throw new AppError("INVALID_REQUEST", "Nieuw wachtwoord moet anders zijn dan het huidige", 400);
}
const user = await prisma.user.findUnique({ where: { id: authUser.id } });
if (!user || !(await verifyPassword(user.passwordHash, currentPassword))) {
throw new AppError("INVALID_CREDENTIALS", "Huidig wachtwoord is onjuist", 401);
}
await prisma.user.update({
where: { id: user.id },
data: { passwordHash: await hashPassword(newPassword) },
});
// Andere sessies intrekken; huidige cookie blijft geldig.
const token = request.cookies[SESSION_COOKIE];
if (token) {
await prisma.adminSession.deleteMany({
where: {
userId: user.id,
NOT: { tokenHash: hashToken(token) },
},
});
} else {
await prisma.adminSession.deleteMany({ where: { userId: user.id } });
reply.clearCookie(SESSION_COOKIE, { path: "/" });
}
return { ok: true };
});
} }
export interface AuthenticatedRequest extends FastifyRequest { export interface AuthenticatedRequest extends FastifyRequest {