Add admin password change form in Settings.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
d53a4f9297
commit
2489e9b1fc
2 changed files with 142 additions and 0 deletions
|
|
@ -63,6 +63,13 @@ export default function SettingsPage() {
|
|||
const [shelves, setShelves] = useState<ShelfRow[]>([]);
|
||||
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<string | null>(null);
|
||||
const [pwErr, setPwErr] = useState<string | null>(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() {
|
|||
</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" }}>
|
||||
<h2 style={{ fontSize: "1rem", color: "#fafafa", textTransform: "none", marginBottom: "0.5rem" }}>
|
||||
Bibliotheek-planken (Stremio)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue