Implement access controlled edit schedule endpoint
Add PATCH /api/schedule endpoint for editing the schedule in a manner that's access controlled.
This commit is contained in:
parent
bb306ee938
commit
5255ed698e
3 changed files with 121 additions and 0 deletions
88
server/api/schedule.patch.ts
Normal file
88
server/api/schedule.patch.ts
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
import type { SchedulePatch } from "~/shared/types/schedule";
|
||||||
|
import { readAccounts, readSchedule, writeSchedule } from "~/server/database";
|
||||||
|
import { broadcastUpdate } from "~/server/streams";
|
||||||
|
import { applyChangeArray } from "~/shared/utils/changes";
|
||||||
|
|
||||||
|
function isChange(change: unknown) {
|
||||||
|
return (
|
||||||
|
typeof change === "object"
|
||||||
|
&& change !== null
|
||||||
|
&& "op" in change
|
||||||
|
&& (
|
||||||
|
change.op === "set" || change.op === "del"
|
||||||
|
)
|
||||||
|
&& "data" in change
|
||||||
|
&& typeof change.data === "object"
|
||||||
|
&& change.data !== null
|
||||||
|
&& "id" in change.data
|
||||||
|
&& typeof change.data.id === "string"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isChangeArray(data: unknown) {
|
||||||
|
return data instanceof Array && data.every(item => isChange(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPatch(data: unknown): SchedulePatch {
|
||||||
|
if (
|
||||||
|
typeof data !== "object"
|
||||||
|
|| data === null
|
||||||
|
|| data instanceof Array
|
||||||
|
|| "locations" in data && !isChangeArray(data.locations)
|
||||||
|
|| "events" in data && !isChangeArray(data.events)
|
||||||
|
|| "roles" in data && !isChangeArray(data.roles)
|
||||||
|
|| "rota" in data && !isChangeArray(data.rota)
|
||||||
|
)
|
||||||
|
throw new Error("Invalid patch data")
|
||||||
|
return data // TODO: Actually validate the whole structure with e.g ajv or zod
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const session = await requireAccountSession(event);
|
||||||
|
const accounts = await readAccounts();
|
||||||
|
const account = accounts.find(a => a.id === session.accountId);
|
||||||
|
if (!account) {
|
||||||
|
throw new Error("Account does not exist");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (account.type !== "admin" && account.type !== "crew") {
|
||||||
|
throw createError({
|
||||||
|
status: 403,
|
||||||
|
statusMessage: "Forbidden",
|
||||||
|
message: "Only crew and admin accounts can edit the schedule.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const schedule = await readSchedule();
|
||||||
|
const patch = await readValidatedBody(event, isPatch);
|
||||||
|
|
||||||
|
// Validate edit restrictions for crew
|
||||||
|
if (account.type === "crew") {
|
||||||
|
if (patch.locations?.length) {
|
||||||
|
throw createError({
|
||||||
|
status: 403,
|
||||||
|
statusMessage: "Forbidden",
|
||||||
|
message: "Only admin accounts can edit locations.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (const event of patch.events ?? []) {
|
||||||
|
const id = event.op === "set" ? event.data.id : event.id;
|
||||||
|
const original = schedule.events.find(e => e.id === id);
|
||||||
|
if (original && !original.crew) {
|
||||||
|
throw createError({
|
||||||
|
status: 403,
|
||||||
|
statusMessage: "Forbidden",
|
||||||
|
message: "Only admin accounts can edit public events.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (patch.events) applyChangeArray(patch.events, schedule.events);
|
||||||
|
if (patch.locations) applyChangeArray(patch.locations, schedule.locations);
|
||||||
|
if (patch.roles) applyChangeArray(patch.roles, schedule.roles = schedule.roles ?? []);
|
||||||
|
if (patch.rota) applyChangeArray(patch.rota, schedule.rota = schedule.rota ?? []);
|
||||||
|
|
||||||
|
await writeSchedule(schedule);
|
||||||
|
await broadcastUpdate(schedule);
|
||||||
|
})
|
12
shared/types/schedule.d.ts
vendored
12
shared/types/schedule.d.ts
vendored
|
@ -49,3 +49,15 @@ export interface Schedule {
|
||||||
roles?: Role[],
|
roles?: Role[],
|
||||||
rota?: Shift[],
|
rota?: Shift[],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ChangeRecord<T extends { id: string }> =
|
||||||
|
| { op: "set", data: T }
|
||||||
|
| { op: "del", data: { id: string }}
|
||||||
|
;
|
||||||
|
|
||||||
|
export interface SchedulePatch {
|
||||||
|
locations?: ChangeRecord<ScheduleLocation>[],
|
||||||
|
events?: ChangeRecord<ScheduleEvent>[],
|
||||||
|
roles?: ChangeRecord<Role>[],
|
||||||
|
rota?: ChangeRecord<Shift>[],
|
||||||
|
}
|
||||||
|
|
21
shared/utils/changes.ts
Normal file
21
shared/utils/changes.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import type { ChangeRecord } from "~/shared/types/schedule";
|
||||||
|
|
||||||
|
export function applyChange<T extends { id: string }>(change: ChangeRecord<T>, data: T[]) {
|
||||||
|
const index = data.findIndex(item => item.id === change.data.id);
|
||||||
|
if (change.op === "del") {
|
||||||
|
if (index !== -1)
|
||||||
|
data.splice(index, 1);
|
||||||
|
} else if (change.op === "set") {
|
||||||
|
if (index !== -1)
|
||||||
|
data.splice(index, 1, change.data);
|
||||||
|
else
|
||||||
|
data.push(change.data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyChangeArray<T extends { id: string }>(changes: ChangeRecord<T>[], data: T[]) {
|
||||||
|
// Note: quadratic complexity due to findIndex in applyChange
|
||||||
|
for (const change of changes) {
|
||||||
|
applyChange(change, data);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue