Passwoerter verdeckt, Kontobereich mit eigenem Passwortwechsel
Zwei berechtigte Rueckmeldungen von Florian: 1. In der Benutzerverwaltung standen die Passwortfelder auf type='text', das Passwort war also im Klartext lesbar - vor fremden Augen und in jeder Bildschirmuebertragung. Alle Passwortfelder nutzen jetzt ein gemeinsames PasswordInput: verdeckt als Voreinstellung, mit Auge zum Aufdecken. Weil ein Administrator das Passwort weitergeben muss, gibt es daneben einen Generator fuer ein zufaelliges, vorlesbares Passwort in Vierergruppen. 2. Es gab keinen Kontobereich. Neu ist /konto mit Profil, den eigenen Gruppen samt Rollenerklaerung, Passwortwechsel und Abmelden. Im Kopfbereich fuehrt ein Namenskuerzel dorthin. Ist die Anmeldung ausgeschaltet, steht dort 'Ohne Anmeldung' und die Seite erklaert, wie man sie einschaltet - vorher suchte man den Abmelden-Knopf vergeblich. Dabei eine echte Luecke geschlossen: ein Benutzer konnte sein Passwort gar nicht selbst aendern, nur ein Administrator konnte es setzen. Der Wechsel verlangt das bisherige Passwort, lehnt ein identisches ab und beendet alle anderen Sitzungen - die eigene bleibt bestehen. Nebenbei: die Betriebsart-Knoepfe im Generator haben jetzt data-mode. Ueber aria-pressed waren sie nicht mehr eindeutig, seit das Passwortfeld einen eigenen Umschalter hat. Geprueft: 63 Pruefungen fuer Anmeldung inkl. Passwortwechsel und Konto, dazu 280 bestehende bei ausgeschalteter Anmeldung.
This commit is contained in:
@@ -8,7 +8,11 @@ import {
|
||||
MIN_PASSWORD_LENGTH,
|
||||
verifyPassword,
|
||||
} from "@/lib/auth/password";
|
||||
import { createSession, destroySession } from "@/lib/auth/session";
|
||||
import {
|
||||
createSession,
|
||||
destroyAllSessions,
|
||||
destroySession,
|
||||
} from "@/lib/auth/session";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { errorResponse, firstZodMessage, jsonError } from "@/lib/http";
|
||||
import { QrRequestError } from "@/lib/qr/type-module";
|
||||
@@ -46,10 +50,21 @@ const setupSchema = z.object({
|
||||
|
||||
const logoutSchema = z.object({ action: z.literal("logout") });
|
||||
|
||||
const changePasswordSchema = z.object({
|
||||
action: z.literal("change-password"),
|
||||
currentPassword: z.string().min(1, {
|
||||
error: "Bitte gib dein bisheriges Passwort ein.",
|
||||
}),
|
||||
newPassword: z.string().min(MIN_PASSWORD_LENGTH, {
|
||||
error: `Das neue Passwort ist zu kurz. ${describePasswordRules()}`,
|
||||
}),
|
||||
});
|
||||
|
||||
const bodySchema = z.discriminatedUnion("action", [
|
||||
loginSchema,
|
||||
setupSchema,
|
||||
logoutSchema,
|
||||
changePasswordSchema,
|
||||
]);
|
||||
|
||||
/** Wer ist angemeldet? Von der Oberfläche für den Kopfbereich genutzt. */
|
||||
@@ -96,6 +111,9 @@ export async function POST(request: NextRequest): Promise<Response> {
|
||||
// Das `await` ist hier nicht optional: ohne es verlässt das Promise den
|
||||
// try-Block, bevor es sich auflöst — eine Ablehnung käme dann nie im
|
||||
// catch an und würde als nackter 500er beim Aufrufer landen.
|
||||
if (parsed.data.action === "change-password") {
|
||||
return await handleChangePassword(parsed.data);
|
||||
}
|
||||
if (parsed.data.action === "setup") {
|
||||
return await handleSetup(parsed.data);
|
||||
}
|
||||
@@ -105,6 +123,51 @@ export async function POST(request: NextRequest): Promise<Response> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Eigenes Passwort ändern. Das bisherige muss stimmen — sonst könnte jemand
|
||||
* an einem unbeaufsichtigten Rechner das Konto übernehmen.
|
||||
*/
|
||||
async function handleChangePassword(
|
||||
input: z.infer<typeof changePasswordSchema>,
|
||||
): Promise<Response> {
|
||||
const actor = await getActor();
|
||||
if (actor.kind !== "user") {
|
||||
throw new QrRequestError("Bitte melde dich an.", 401);
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({ where: { id: actor.user.id } });
|
||||
if (!user) {
|
||||
throw new QrRequestError("Dieses Konto existiert nicht mehr.", 404);
|
||||
}
|
||||
|
||||
if (!(await verifyPassword(input.currentPassword, user.passwordHash))) {
|
||||
throw new QrRequestError("Das bisherige Passwort stimmt nicht.", 403);
|
||||
}
|
||||
|
||||
if (input.currentPassword === input.newPassword) {
|
||||
throw new QrRequestError(
|
||||
"Das neue Passwort ist mit dem bisherigen identisch.",
|
||||
);
|
||||
}
|
||||
|
||||
await prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: {
|
||||
passwordHash: await hashPassword(input.newPassword),
|
||||
failedLogins: 0,
|
||||
lockedUntil: null,
|
||||
},
|
||||
});
|
||||
|
||||
// Alle Sitzungen beenden — auch die eigene — und sofort eine neue
|
||||
// aufmachen. So fliegen andere Geräte raus, ohne dass man sich hier neu
|
||||
// anmelden muss.
|
||||
await destroyAllSessions(user.id);
|
||||
await createSession(user.id);
|
||||
|
||||
return Response.json({ ok: true });
|
||||
}
|
||||
|
||||
/** Legt den ersten Benutzer an — nur solange es noch keinen gibt. */
|
||||
async function handleSetup(
|
||||
input: z.infer<typeof setupSchema>,
|
||||
|
||||
Reference in New Issue
Block a user