119 lines
3.3 KiB
TypeScript
119 lines
3.3 KiB
TypeScript
import { call, os } from "@orpc/server";
|
|
import { z } from "zod";
|
|
import { randomUUID } from "crypto";
|
|
import { createKV } from "@/server/lib/create-kv";
|
|
|
|
const AvailabilitySchema = z.object({
|
|
id: z.string(),
|
|
date: z.string(), // YYYY-MM-DD
|
|
time: z.string(), // HH:MM
|
|
durationMinutes: z.number().int().positive(),
|
|
status: z.enum(["free", "reserved"]),
|
|
reservedByBookingId: z.string().optional(),
|
|
createdAt: z.string(),
|
|
});
|
|
|
|
export type Availability = z.output<typeof AvailabilitySchema>;
|
|
|
|
const kv = createKV<Availability>("availability");
|
|
|
|
// Minimal Owner-Prüfung über Sessions/Users KV
|
|
type Session = { id: string; userId: string; expiresAt: string; createdAt: string };
|
|
type User = { id: string; username: string; email: string; passwordHash: string; role: "customer" | "owner"; createdAt: string };
|
|
const sessionsKV = createKV<Session>("sessions");
|
|
const usersKV = createKV<User>("users");
|
|
|
|
async function assertOwner(sessionId: string): Promise<void> {
|
|
const session = await sessionsKV.getItem(sessionId);
|
|
if (!session) throw new Error("Invalid session");
|
|
if (new Date(session.expiresAt) < new Date()) throw new Error("Session expired");
|
|
const user = await usersKV.getItem(session.userId);
|
|
if (!user || user.role !== "owner") throw new Error("Forbidden");
|
|
}
|
|
|
|
const create = os
|
|
.input(
|
|
z.object({
|
|
sessionId: z.string(),
|
|
date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
|
|
time: z.string().regex(/^\d{2}:\d{2}$/),
|
|
durationMinutes: z.number().int().positive(),
|
|
})
|
|
)
|
|
.handler(async ({ input }) => {
|
|
await assertOwner(input.sessionId);
|
|
const id = randomUUID();
|
|
const slot: Availability = {
|
|
id,
|
|
date: input.date,
|
|
time: input.time,
|
|
durationMinutes: input.durationMinutes,
|
|
status: "free",
|
|
createdAt: new Date().toISOString(),
|
|
};
|
|
await kv.setItem(id, slot);
|
|
return slot;
|
|
});
|
|
|
|
const update = os
|
|
.input(AvailabilitySchema.extend({ sessionId: z.string() }))
|
|
.handler(async ({ input }) => {
|
|
await assertOwner(input.sessionId);
|
|
const { sessionId, ...rest } = input as any;
|
|
await kv.setItem(rest.id, rest as Availability);
|
|
return rest as Availability;
|
|
});
|
|
|
|
const remove = os
|
|
.input(z.object({ sessionId: z.string(), id: z.string() }))
|
|
.handler(async ({ input }) => {
|
|
await assertOwner(input.sessionId);
|
|
const slot = await kv.getItem(input.id);
|
|
if (slot && slot.status === "reserved") throw new Error("Cannot delete reserved slot");
|
|
await kv.removeItem(input.id);
|
|
});
|
|
|
|
const list = os.handler(async () => {
|
|
return kv.getAllItems();
|
|
});
|
|
|
|
const get = os.input(z.string()).handler(async ({ input }) => {
|
|
return kv.getItem(input);
|
|
});
|
|
|
|
const getByDate = os
|
|
.input(z.string()) // YYYY-MM-DD
|
|
.handler(async ({ input }) => {
|
|
const all = await kv.getAllItems();
|
|
return all.filter((s) => s.date === input);
|
|
});
|
|
|
|
const live = {
|
|
list: os.handler(async function* ({ signal }) {
|
|
yield call(list, {}, { signal });
|
|
for await (const _ of kv.subscribe()) {
|
|
yield call(list, {}, { signal });
|
|
}
|
|
}),
|
|
byDate: os
|
|
.input(z.string())
|
|
.handler(async function* ({ input, signal }) {
|
|
yield call(getByDate, input, { signal });
|
|
for await (const _ of kv.subscribe()) {
|
|
yield call(getByDate, input, { signal });
|
|
}
|
|
}),
|
|
};
|
|
|
|
export const router = {
|
|
create,
|
|
update,
|
|
remove,
|
|
list,
|
|
get,
|
|
getByDate,
|
|
live,
|
|
};
|
|
|
|
|