181 lines
5.3 KiB
TypeScript
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);
|
|
}
|
|
}
|