From 2489e9b1fcfe7a4f94eab1f698accefc68d53cb0 Mon Sep 17 00:00:00 2001 From: Jos Vooges | STH Date: Sat, 29 Aug 2026 02:19:33 +0200 Subject: [PATCH] Add admin password change form in Settings. Co-authored-by: Cursor --- apps/admin-ui/src/app/settings/page.tsx | 90 +++++++++++++++++++++++++ apps/master-api/src/auth/routes.ts | 52 ++++++++++++++ 2 files changed, 142 insertions(+) diff --git a/apps/admin-ui/src/app/settings/page.tsx b/apps/admin-ui/src/app/settings/page.tsx index f9fb2d3..86b7fb7 100644 --- a/apps/admin-ui/src/app/settings/page.tsx +++ b/apps/admin-ui/src/app/settings/page.tsx @@ -63,6 +63,13 @@ export default function SettingsPage() { const [shelves, setShelves] = useState([]); const [newShelfName, setNewShelfName] = useState(""); 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(null); + const [pwErr, setPwErr] = useState(null); const loadTokens = useCallback(() => { fetch("/api/v1/admin/addon-tokens", { credentials: "include" }) @@ -79,8 +86,49 @@ export default function SettingsPage() { useEffect(() => { loadTokens(); loadShelves(); + fetch("/api/v1/auth/me", { credentials: "include" }) + .then((r) => r.json()) + .then((d) => setAdminEmail(d.user?.email ?? "")) + .catch(() => undefined); }, [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() { if (!newShelfName.trim()) return; const res = await fetch("/api/v1/admin/library/shelves", { @@ -288,6 +336,48 @@ export default function SettingsPage() { +
+

+ Account +

+

+ Ingelogd als {adminEmail || "…"}. + Wijzig hier je admin-wachtwoord (min. 8 tekens). +

+ {pwErr &&
{pwErr}
} + {pwMsg &&

{pwMsg}

} +
+ + setCurrentPassword(e.target.value)} + autoComplete="current-password" + /> +
+
+ + setNewPassword(e.target.value)} + autoComplete="new-password" + /> +
+
+ + setConfirmPassword(e.target.value)} + autoComplete="new-password" + /> +
+ +
+

Bibliotheek-planken (Stremio) diff --git a/apps/master-api/src/auth/routes.ts b/apps/master-api/src/auth/routes.ts index dfc57e3..5f4ab3f 100644 --- a/apps/master-api/src/auth/routes.ts +++ b/apps/master-api/src/auth/routes.ts @@ -69,6 +69,58 @@ export async function registerAuthRoutes( const user = (request as AuthenticatedRequest).user; 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 {