Initial commit: Kalendartool (Next.js, Prisma, Docker)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
159
lib/validation.ts
Normal file
159
lib/validation.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* Eingabevalidierung mit Zod (plan.md: "Jede Eingabe gilt als unsicher").
|
||||
* Alle API-Routen validieren Request-Bodies ausschliesslich ueber diese Schemas.
|
||||
*/
|
||||
import { z } from 'zod';
|
||||
|
||||
/** Sichere E-Mail (Zod prueft Format, wir begrenzen die Laenge). */
|
||||
export const emailSchema = z
|
||||
.string()
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.min(3, 'E-Mail ist erforderlich.')
|
||||
.max(254, 'E-Mail ist zu lang.')
|
||||
.email('Ungueltige E-Mail-Adresse.');
|
||||
|
||||
/**
|
||||
* Passwortregeln: mindestens 10 Zeichen, Gross-/Kleinbuchstabe und Ziffer.
|
||||
* Maximale Laenge begrenzen (DoS-Schutz bei bcrypt).
|
||||
*/
|
||||
export const passwordSchema = z
|
||||
.string()
|
||||
.min(10, 'Das Passwort muss mindestens 10 Zeichen lang sein.')
|
||||
.max(72, 'Das Passwort darf hoechstens 72 Zeichen lang sein.')
|
||||
.regex(/[a-z]/, 'Das Passwort muss einen Kleinbuchstaben enthalten.')
|
||||
.regex(/[A-Z]/, 'Das Passwort muss einen Grossbuchstaben enthalten.')
|
||||
.regex(/[0-9]/, 'Das Passwort muss eine Ziffer enthalten.');
|
||||
|
||||
/**
|
||||
* Username: 2-32 Zeichen, Buchstaben, Ziffern, Unterstrich, Bindestrich,
|
||||
* Punkt. Keine Leerzeichen oder Sonderzeichen (verhindert Spoofing und
|
||||
* Injection in der Anzeige). Case-insensitive Eindeutigkeit wird in der
|
||||
* DB sichergestellt; hier wird nur das Format geprueft.
|
||||
*/
|
||||
export const usernameSchema = z
|
||||
.string()
|
||||
.trim()
|
||||
.min(2, 'Der Benutzername muss mindestens 2 Zeichen lang sein.')
|
||||
.max(32, 'Der Benutzername darf hoechstens 32 Zeichen lang sein.')
|
||||
.regex(
|
||||
/^[a-zA-Z0-9_.-]+$/,
|
||||
'Der Benutzername darf nur Buchstaben, Ziffern, Unterstrich, Bindestrich und Punkt enthalten.',
|
||||
);
|
||||
|
||||
export const registerSchema = z
|
||||
.object({
|
||||
email: emailSchema,
|
||||
password: passwordSchema,
|
||||
username: usernameSchema.optional(),
|
||||
inviteToken: z.string().trim().min(10).max(200).optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const loginSchema = z
|
||||
.object({
|
||||
email: emailSchema,
|
||||
password: z.string().min(1, 'Passwort ist erforderlich.').max(72),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const createCalendarSchema = z
|
||||
.object({
|
||||
title: z.string().trim().min(1, 'Titel ist erforderlich.').max(120),
|
||||
description: z.string().trim().max(2000).optional().nullable(),
|
||||
timezone: z.string().trim().min(1).max(64),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const updateCalendarSchema = createCalendarSchema
|
||||
.partial()
|
||||
.extend({
|
||||
isActive: z.boolean().optional(),
|
||||
/** Maximale Reservierungsdauer in Minuten; 0 = unbegrenzt. */
|
||||
maxReservationMinutes: z
|
||||
.number()
|
||||
.int('Die maximale Dauer muss ein Vielfaches von 15 Minuten sein.')
|
||||
.min(0, 'Die maximale Dauer kann nicht negativ sein.')
|
||||
.max(72 * 60, 'Die maximale Dauer betraegt hoechstens 72 Stunden.')
|
||||
.multipleOf(15, 'Die maximale Dauer muss ein Vielfaches von 15 Minuten sein.')
|
||||
.optional(),
|
||||
/** Duerven Nicht-Admins die Titel fremder Reservierungen sehen? */
|
||||
showTitleToMembers: z.boolean().optional(),
|
||||
/** Duerven Nicht-Admins die Notizen fremder Reservierungen sehen? */
|
||||
showNotesToMembers: z.boolean().optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
/** Dauer in Minuten: 15 Minuten bis 24 Stunden, in 15-Minuten-Schritten. */
|
||||
export const durationSchema = z
|
||||
.number()
|
||||
.int('Die Dauer muss ein Vielfaches von 15 Minuten sein.')
|
||||
.min(15, 'Die Mindestdauer betraegt 15 Minuten.')
|
||||
.max(24 * 60, 'Die Maximaldauer betraegt 24 Stunden.')
|
||||
.multipleOf(15, 'Die Dauer muss ein Vielfaches von 15 Minuten sein.');
|
||||
|
||||
/**
|
||||
* Reservierung anlegen: Start- UND Endzeit werden angegeben. Die
|
||||
* Dauer ergibt sich aus der Differenz; die maximale Dauer wird
|
||||
* serverseitig gegen die Kalender-Einstellung geprueft.
|
||||
*/
|
||||
export const createReservationSchema = z
|
||||
.object({
|
||||
calendarId: z.string().trim().min(1),
|
||||
startAt: z.coerce.date(),
|
||||
endAt: z.coerce.date(),
|
||||
title: z.string().trim().min(1, 'Titel ist erforderlich.').max(120),
|
||||
notes: z.string().trim().max(2000).optional().nullable(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const updateReservationSchema = z
|
||||
.object({
|
||||
startAt: z.coerce.date().optional(),
|
||||
endAt: z.coerce.date().optional(),
|
||||
title: z.string().trim().min(1).max(120).optional(),
|
||||
notes: z.string().trim().max(2000).optional().nullable(),
|
||||
status: z.enum(['CONFIRMED', 'CANCELLED']).optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const createInviteSchema = z
|
||||
.object({
|
||||
calendarId: z.string().trim().min(1),
|
||||
expiresInDays: z.number().int().min(1).max(90).optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
/**
|
||||
* Mitglied per E-Mail zu einem Kalender hinzufuegen (DB-basiertes
|
||||
* Einladungssystem, ersetzt den Token-Link-Flow in der Verwaltung).
|
||||
*/
|
||||
export const addMemberSchema = z
|
||||
.object({
|
||||
email: emailSchema,
|
||||
})
|
||||
.strict();
|
||||
|
||||
/** Rollenaenderung (Userverwaltung): nur die beiden bekannten Rollen. */
|
||||
export const updateRoleSchema = z
|
||||
.object({
|
||||
role: z.enum(['ADMIN', 'USER'], {
|
||||
errorMap: () => ({ message: 'Rolle muss ADMIN oder USER sein.' }),
|
||||
}),
|
||||
})
|
||||
.strict();
|
||||
|
||||
/** Berechnet die Endzeit aus Startzeit und Dauer in Minuten. */
|
||||
export function computeEndAt(startAt: Date, durationMinutes: number): Date {
|
||||
return new Date(startAt.getTime() + durationMinutes * 60_000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Username im Profil aendern. Der Wert ist optional (null = loeschen),
|
||||
* aber wenn er gesetzt ist, muss er dem Format entsprechen.
|
||||
*/
|
||||
export const updateUsernameSchema = z
|
||||
.object({
|
||||
username: usernameSchema.nullable(),
|
||||
})
|
||||
.strict();
|
||||
Reference in New Issue
Block a user