175 lines
5.4 KiB
TypeScript
175 lines
5.4 KiB
TypeScript
export const dynamic = "force-dynamic";
|
|
|
|
import { z } from "zod";
|
|
import { requireAdmin } from "@/lib/auth/session";
|
|
import { handleAuthError, fail, ok } from "@/lib/api";
|
|
import {
|
|
deletePersonCalendarResource,
|
|
updatePersonCalendarResource
|
|
} from "@/lib/services/person-calendar-resources";
|
|
import { readJsonBody, validateMutationRequestOrigin } from "@/lib/security/request";
|
|
import {
|
|
deriveLegacyAvailability,
|
|
hasAtLeastOneEnabledDay,
|
|
normalizeWeekdayAvailability,
|
|
serializeWeekdayAvailability
|
|
} from "@/lib/weekday-availability";
|
|
|
|
const TIME_RE = /^([01]\d|2[0-3]):[0-5]\d$/;
|
|
|
|
const dayRangeSchema = z.object({
|
|
enabled: z.boolean(),
|
|
start: z.string().regex(TIME_RE),
|
|
end: z.string().regex(TIME_RE)
|
|
});
|
|
|
|
const weekdayRangesSchema = z
|
|
.object({
|
|
"0": dayRangeSchema,
|
|
"1": dayRangeSchema,
|
|
"2": dayRangeSchema,
|
|
"3": dayRangeSchema,
|
|
"4": dayRangeSchema,
|
|
"5": dayRangeSchema,
|
|
"6": dayRangeSchema
|
|
})
|
|
.superRefine((ranges, ctx) => {
|
|
for (const day of ["0", "1", "2", "3", "4", "5", "6"] as const) {
|
|
const value = ranges[day];
|
|
if (value.enabled && value.start >= value.end) {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
message: "Ungültiges Zeitfenster",
|
|
path: [day, "end"]
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
const updateSchema = z
|
|
.object({
|
|
resourceName: z.string().trim().min(2).max(120).optional(),
|
|
resourceBio: z.string().trim().max(500).optional(),
|
|
isActive: z.boolean().optional(),
|
|
calendarName: z.string().trim().min(2).max(120).optional(),
|
|
bookingDayRanges: weekdayRangesSchema.optional(),
|
|
bookingAllowedWeekdays: z
|
|
.string()
|
|
.regex(/^([0-6](,[0-6])*)$/)
|
|
.optional(),
|
|
bookingDayStartTime: z
|
|
.string()
|
|
.regex(TIME_RE)
|
|
.optional(),
|
|
bookingDayEndTime: z
|
|
.string()
|
|
.regex(TIME_RE)
|
|
.optional(),
|
|
url: z.string().url().max(1024).optional(),
|
|
username: z.string().trim().min(1).max(160).optional(),
|
|
notificationEmail: z.string().trim().email().max(320).optional(),
|
|
password: z.string().min(1).max(2000).optional(),
|
|
color: z.string().trim().max(64).optional(),
|
|
syncEnabled: z.boolean().optional()
|
|
})
|
|
.superRefine((data, ctx) => {
|
|
if (data.bookingDayRanges && !Object.values(data.bookingDayRanges).some((day) => day.enabled)) {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
message: "Mindestens ein Wochentag muss aktiv sein.",
|
|
path: ["bookingDayRanges", "0", "enabled"]
|
|
});
|
|
}
|
|
|
|
if (
|
|
data.bookingDayStartTime &&
|
|
data.bookingDayEndTime &&
|
|
data.bookingDayStartTime >= data.bookingDayEndTime
|
|
) {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
message: "Ungültiges Zeitfenster",
|
|
path: ["bookingDayEndTime"]
|
|
});
|
|
}
|
|
});
|
|
|
|
export async function PATCH(
|
|
req: Request,
|
|
{ params }: { params: Promise<{ id: string }> }
|
|
) {
|
|
try {
|
|
const originError = validateMutationRequestOrigin(req);
|
|
if (originError) return originError;
|
|
|
|
await requireAdmin();
|
|
const { id } = await params;
|
|
const bodyResult = await readJsonBody(req, { maxBytes: 64 * 1024 });
|
|
if (!bodyResult.ok) return bodyResult.response;
|
|
const parsed = updateSchema.safeParse(bodyResult.data);
|
|
|
|
if (!parsed.success) {
|
|
return fail("Ungültige Kalenderdaten", 400, parsed.error.flatten());
|
|
}
|
|
|
|
const updatePayload: Parameters<typeof updatePersonCalendarResource>[1] = {
|
|
resourceName: parsed.data.resourceName,
|
|
resourceBio: parsed.data.resourceBio,
|
|
isActive: parsed.data.isActive,
|
|
calendarName: parsed.data.calendarName,
|
|
bookingAllowedWeekdays: parsed.data.bookingAllowedWeekdays,
|
|
bookingDayStartTime: parsed.data.bookingDayStartTime,
|
|
bookingDayEndTime: parsed.data.bookingDayEndTime,
|
|
url: parsed.data.url,
|
|
username: parsed.data.username,
|
|
notificationEmail: parsed.data.notificationEmail,
|
|
password: parsed.data.password,
|
|
color: parsed.data.color,
|
|
syncEnabled: parsed.data.syncEnabled
|
|
};
|
|
|
|
if (parsed.data.bookingDayRanges) {
|
|
const normalizedRanges = normalizeWeekdayAvailability(parsed.data.bookingDayRanges);
|
|
if (!hasAtLeastOneEnabledDay(normalizedRanges)) {
|
|
return fail("Mindestens ein aktiver Wochentag mit gültiger Uhrzeit ist erforderlich.", 400);
|
|
}
|
|
const legacy = deriveLegacyAvailability(normalizedRanges);
|
|
updatePayload.bookingAllowedWeekdays = legacy.bookingAllowedWeekdays;
|
|
updatePayload.bookingDayStartTime = legacy.bookingDayStartTime;
|
|
updatePayload.bookingDayEndTime = legacy.bookingDayEndTime;
|
|
updatePayload.bookingDayRangesJson = serializeWeekdayAvailability(normalizedRanges);
|
|
}
|
|
|
|
const resource = await updatePersonCalendarResource(id, updatePayload);
|
|
if (!resource) {
|
|
return fail("Personen-Kalender nicht gefunden", 404);
|
|
}
|
|
|
|
return ok({ resource });
|
|
} catch (error) {
|
|
return handleAuthError(error);
|
|
}
|
|
}
|
|
|
|
export async function DELETE(
|
|
req: Request,
|
|
{ params }: { params: Promise<{ id: string }> }
|
|
) {
|
|
try {
|
|
const originError = validateMutationRequestOrigin(req);
|
|
if (originError) return originError;
|
|
|
|
await requireAdmin();
|
|
const { id } = await params;
|
|
const deleted = await deletePersonCalendarResource(id);
|
|
|
|
if (!deleted) {
|
|
return fail("Personen-Kalender nicht gefunden", 404);
|
|
}
|
|
|
|
return ok({ message: "Personen-Kalender entfernt" });
|
|
} catch (error) {
|
|
return handleAuthError(error);
|
|
}
|
|
}
|