139 lines
4.8 KiB
JavaScript
139 lines
4.8 KiB
JavaScript
import { call, os } from "@orpc/server";
|
|
import { z } from "zod";
|
|
import { randomUUID } from "crypto";
|
|
import { createKV } from "../lib/create-kv.js";
|
|
import { assertOwner } from "../lib/auth.js";
|
|
import { enforceAdminRateLimit } from "../lib/rate-limiter.js";
|
|
// Schema Definition
|
|
const GalleryPhotoSchema = z.object({
|
|
id: z.string(),
|
|
base64Data: z.string(),
|
|
title: z.string().optional().default(""),
|
|
order: z.number().int(),
|
|
createdAt: z.string(),
|
|
cover: z.boolean().optional().default(false),
|
|
});
|
|
// KV Storage
|
|
const galleryPhotosKV = createKV("galleryPhotos");
|
|
// Authentication centralized in ../lib/auth.ts
|
|
// CRUD Endpoints
|
|
const uploadPhoto = os
|
|
.input(z.object({
|
|
base64Data: z
|
|
.string()
|
|
.regex(/^data:image\/(png|jpe?g|webp|gif);base64,/i, 'Unsupported image format'),
|
|
title: z.string().optional().default(""),
|
|
}))
|
|
.handler(async ({ input, context }) => {
|
|
try {
|
|
await assertOwner(context);
|
|
// Admin Rate Limiting
|
|
await enforceAdminRateLimit(context);
|
|
const id = randomUUID();
|
|
const existing = await galleryPhotosKV.getAllItems();
|
|
const maxOrder = existing.length > 0 ? Math.max(...existing.map((p) => p.order)) : -1;
|
|
const nextOrder = maxOrder + 1;
|
|
const photo = {
|
|
id,
|
|
base64Data: input.base64Data,
|
|
title: input.title ?? "",
|
|
order: nextOrder,
|
|
createdAt: new Date().toISOString(),
|
|
cover: false,
|
|
};
|
|
await galleryPhotosKV.setItem(id, photo);
|
|
return photo;
|
|
}
|
|
catch (err) {
|
|
console.error("gallery.uploadPhoto error", err);
|
|
throw err;
|
|
}
|
|
});
|
|
const setCoverPhoto = os
|
|
.input(z.object({ id: z.string() }))
|
|
.handler(async ({ input, context }) => {
|
|
await assertOwner(context);
|
|
// Admin Rate Limiting
|
|
await enforceAdminRateLimit(context);
|
|
const all = await galleryPhotosKV.getAllItems();
|
|
let updatedCover = null;
|
|
for (const p of all) {
|
|
const isCover = p.id === input.id;
|
|
const next = { ...p, cover: isCover };
|
|
await galleryPhotosKV.setItem(p.id, next);
|
|
if (isCover)
|
|
updatedCover = next;
|
|
}
|
|
return updatedCover;
|
|
});
|
|
const deletePhoto = os
|
|
.input(z.object({ id: z.string() }))
|
|
.handler(async ({ input, context }) => {
|
|
await assertOwner(context);
|
|
// Admin Rate Limiting
|
|
await enforceAdminRateLimit(context);
|
|
await galleryPhotosKV.removeItem(input.id);
|
|
});
|
|
const updatePhotoOrder = os
|
|
.input(z.object({
|
|
photoOrders: z.array(z.object({ id: z.string(), order: z.number().int() })),
|
|
}))
|
|
.handler(async ({ input, context }) => {
|
|
await assertOwner(context);
|
|
// Admin Rate Limiting
|
|
await enforceAdminRateLimit(context);
|
|
const updated = [];
|
|
for (const { id, order } of input.photoOrders) {
|
|
const existing = await galleryPhotosKV.getItem(id);
|
|
if (!existing)
|
|
continue;
|
|
const updatedPhoto = { ...existing, order };
|
|
await galleryPhotosKV.setItem(id, updatedPhoto);
|
|
updated.push(updatedPhoto);
|
|
}
|
|
const all = await galleryPhotosKV.getAllItems();
|
|
return all.sort((a, b) => (a.order - b.order) || a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id));
|
|
});
|
|
const listPhotos = os.handler(async () => {
|
|
const all = await galleryPhotosKV.getAllItems();
|
|
return all.sort((a, b) => (a.order - b.order) || a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id));
|
|
});
|
|
const adminListPhotos = os
|
|
.input(z.object({}))
|
|
.handler(async ({ context }) => {
|
|
await assertOwner(context);
|
|
const all = await galleryPhotosKV.getAllItems();
|
|
return all.sort((a, b) => (a.order - b.order) || a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id));
|
|
});
|
|
// Live Queries
|
|
const live = {
|
|
listPhotos: os.handler(async function* ({ signal }) {
|
|
yield call(listPhotos, {}, { signal });
|
|
for await (const _ of galleryPhotosKV.subscribe()) {
|
|
yield call(listPhotos, {}, { signal });
|
|
}
|
|
}),
|
|
adminListPhotos: os
|
|
.input(z.object({}))
|
|
.handler(async function* ({ context, signal }) {
|
|
await assertOwner(context);
|
|
const all = await galleryPhotosKV.getAllItems();
|
|
const sorted = all.sort((a, b) => (a.order - b.order) || a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id));
|
|
yield sorted;
|
|
for await (const _ of galleryPhotosKV.subscribe()) {
|
|
const updated = await galleryPhotosKV.getAllItems();
|
|
const sortedUpdated = updated.sort((a, b) => (a.order - b.order) || a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id));
|
|
yield sortedUpdated;
|
|
}
|
|
}),
|
|
};
|
|
export const router = {
|
|
uploadPhoto,
|
|
deletePhoto,
|
|
updatePhotoOrder,
|
|
listPhotos,
|
|
adminListPhotos,
|
|
setCoverPhoto,
|
|
live,
|
|
};
|