Replace all async reads and writes to the JSON database with the sync reads and writes to prevent a data corruption race condition where two requests are processed at the same time and write to the same file, or one reads while the other writes causing read of partially written data.
95 lines
2.7 KiB
TypeScript
95 lines
2.7 KiB
TypeScript
/*
|
|
SPDX-FileCopyrightText: © 2025 Hornwitser <code@hornwitser.no>
|
|
SPDX-License-Identifier: AGPL-3.0-or-later
|
|
*/
|
|
import { z } from "zod/v4-mini";
|
|
import { nextEventId, readSchedule, writeSchedule } from "~/server/database";
|
|
import { broadcastEvent } from "~/server/streams";
|
|
import { apiScheduleSchema } from "~/shared/types/api";
|
|
import { applyUpdatesToArray } from "~/shared/utils/update";
|
|
|
|
export default defineEventHandler(async (event) => {
|
|
const session = await requireServerSessionWithUser(event);
|
|
|
|
if (session.access !== "admin" && session.access !== "crew") {
|
|
throw createError({
|
|
status: 403,
|
|
statusMessage: "Forbidden",
|
|
message: "Only crew and admin accounts can edit the schedule.",
|
|
});
|
|
}
|
|
|
|
const { success, error, data: update } = apiScheduleSchema.safeParse(await readBody(event));
|
|
if (!success) {
|
|
throw createError({
|
|
status: 400,
|
|
statusText: "Bad Request",
|
|
message: z.prettifyError(error),
|
|
});
|
|
}
|
|
|
|
if (update.deleted) {
|
|
throw createError({
|
|
statusCode: 400,
|
|
statusMessage: "Not implemented",
|
|
});
|
|
}
|
|
|
|
const schedule = readSchedule();
|
|
|
|
if (schedule.deleted) {
|
|
throw createError({
|
|
statusCode: 400,
|
|
statusMessage: "Not implemented",
|
|
});
|
|
}
|
|
|
|
// Validate edit restrictions for crew
|
|
if (session.access === "crew") {
|
|
if (update.locations?.length) {
|
|
throw createError({
|
|
status: 403,
|
|
statusMessage: "Forbidden",
|
|
message: "Only admin accounts can edit locations.",
|
|
});
|
|
}
|
|
for (const event of update.events ?? []) {
|
|
const original = schedule.events?.find(e => e.id === event.id);
|
|
if (original && !original.deleted && !original.crew) {
|
|
throw createError({
|
|
status: 403,
|
|
statusMessage: "Forbidden",
|
|
message: "Only admin accounts can edit public events.",
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update schedule
|
|
const updatedFrom = schedule.updatedAt;
|
|
update.updatedAt = new Date().toISOString();
|
|
if (update.events) {
|
|
for (const event of update.events) event.updatedAt = update.updatedAt;
|
|
applyUpdatesToArray(update.events, schedule.events = schedule.events ?? []);
|
|
}
|
|
if (update.locations) {
|
|
for (const location of update.locations) location.updatedAt = update.updatedAt;
|
|
applyUpdatesToArray(update.locations, schedule.locations = schedule.locations ?? []);
|
|
}
|
|
if (update.roles) {
|
|
for (const role of update.roles) role.updatedAt = update.updatedAt;
|
|
applyUpdatesToArray(update.roles, schedule.roles = schedule.roles ?? []);
|
|
}
|
|
if (update.shifts) {
|
|
for (const shift of update.shifts) shift.updatedAt = update.updatedAt;
|
|
applyUpdatesToArray(update.shifts, schedule.shifts = schedule.shifts ?? []);
|
|
}
|
|
|
|
writeSchedule(schedule);
|
|
await broadcastEvent({
|
|
id: nextEventId(),
|
|
type: "schedule-update",
|
|
updatedFrom,
|
|
data: update,
|
|
});
|
|
})
|