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 [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)
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue