Files
Calbook/app/api/admin/kalender/route.ts

181 lines
5.3 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 {
createPersonCalendarResource,
listPersonCalendarResources
} from "@/lib/services/person-calendar-resources";
import { readJsonBody, validateMutationRequestOrigin } from "@/lib/security/request";
import {
createWeekdayAvailabilityFromLegacy,
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"]
});
}
}
if (!Object.values(ranges).some((value) => value.enabled)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Mindestens ein Wochentag muss aktiv sein.",
path: ["0", "enabled"]
});
}
});
const createSchema = z
.object({
resourceName: z.string().trim().min(2).max(120),
resourceBio: z.string().trim().max(500).optional(),
isActive: z.boolean().default(true),
calendarName: z.string().trim().min(2).max(120),
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(),
username: z.string().trim().min(1).max(160),
notificationEmail: z.string().trim().email().max(320),
password: z.string().min(1).max(2000),
color: z.string().trim().max(64).optional(),
syncEnabled: z.boolean().default(true)
})
.superRefine((data, ctx) => {
if (data.bookingDayRanges) {
return;
}
if (!data.bookingAllowedWeekdays) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Fehlende Wochentage",
path: ["bookingAllowedWeekdays"]
});
}
if (!data.bookingDayStartTime) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Fehlende Startzeit",
path: ["bookingDayStartTime"]
});
}
if (!data.bookingDayEndTime) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Fehlende Endzeit",
path: ["bookingDayEndTime"]
});
}
if (
data.bookingDayStartTime &&
data.bookingDayEndTime &&
data.bookingDayStartTime >= data.bookingDayEndTime
) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Ungültiges Zeitfenster",
path: ["bookingDayEndTime"]
});
}
});
export async function GET() {
try {
await requireAdmin();
const data = await listPersonCalendarResources();
return ok(data);
} catch (error) {
return handleAuthError(error);
}
}
export async function POST(req: Request) {
try {
const originError = validateMutationRequestOrigin(req);
if (originError) return originError;
await requireAdmin();
const bodyResult = await readJsonBody(req, { maxBytes: 64 * 1024 });
if (!bodyResult.ok) return bodyResult.response;
const parsed = createSchema.safeParse(bodyResult.data);
if (!parsed.success) {
return fail("Ungültige Kalenderdaten", 400, parsed.error.flatten());
}
const normalizedRanges = normalizeWeekdayAvailability(
parsed.data.bookingDayRanges
? parsed.data.bookingDayRanges
: createWeekdayAvailabilityFromLegacy(
parsed.data.bookingAllowedWeekdays ?? "0,1,2,3,4",
parsed.data.bookingDayStartTime ?? "09:00",
parsed.data.bookingDayEndTime ?? "17:00"
)
);
if (!hasAtLeastOneEnabledDay(normalizedRanges)) {
return fail("Mindestens ein aktiver Wochentag mit gültiger Uhrzeit ist erforderlich.", 400);
}
const legacy = deriveLegacyAvailability(normalizedRanges);
const resource = await createPersonCalendarResource({
resourceName: parsed.data.resourceName,
resourceBio: parsed.data.resourceBio,
isActive: parsed.data.isActive,
calendarName: parsed.data.calendarName,
bookingAllowedWeekdays: legacy.bookingAllowedWeekdays,
bookingDayStartTime: legacy.bookingDayStartTime,
bookingDayEndTime: legacy.bookingDayEndTime,
bookingDayRangesJson: serializeWeekdayAvailability(normalizedRanges),
url: parsed.data.url,
username: parsed.data.username,
notificationEmail: parsed.data.notificationEmail,
password: parsed.data.password,
color: parsed.data.color,
syncEnabled: parsed.data.syncEnabled
});
return ok({ resource }, 201);
} catch (error) {
return handleAuthError(error);
}
}