Files
Calbook/app/api/admin/kalender/[id]/route.ts

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);
}
}