Email: Review-Link auf /review/:token umgestellt; Token-Erzeugung konsolidiert. Reviews: Client-Validation hinzugefügt. Verfügbarkeiten: Auto-Update nach Regelanlage. Galerie: Cover-Foto-Flag + Setzen im Admin, sofortige Aktualisierung nach Upload/Löschen/Reihenfolge-Änderung. Startseite: Featured-Foto = Reihenfolge 0, Seitenverhältnis beibehalten, Texte aktualisiert.
This commit is contained in:
150
src/server/rpc/gallery.ts
Normal file
150
src/server/rpc/gallery.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
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";
|
||||
|
||||
// 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),
|
||||
});
|
||||
|
||||
export type GalleryPhoto = z.output<typeof GalleryPhotoSchema>;
|
||||
|
||||
// KV Storage
|
||||
const galleryPhotosKV = createKV<GalleryPhoto>("galleryPhotos");
|
||||
|
||||
// Authentication centralized in ../lib/auth.ts
|
||||
|
||||
// CRUD Endpoints
|
||||
const uploadPhoto = os
|
||||
.input(
|
||||
z.object({
|
||||
sessionId: z.string(),
|
||||
base64Data: z
|
||||
.string()
|
||||
.regex(/^data:image\/(png|jpe?g|webp|gif);base64,/i, 'Unsupported image format'),
|
||||
title: z.string().optional().default(""),
|
||||
})
|
||||
)
|
||||
.handler(async ({ input }) => {
|
||||
try {
|
||||
await assertOwner(input.sessionId);
|
||||
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: GalleryPhoto = {
|
||||
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({ sessionId: z.string(), id: z.string() }))
|
||||
.handler(async ({ input }) => {
|
||||
await assertOwner(input.sessionId);
|
||||
const all = await galleryPhotosKV.getAllItems();
|
||||
let updatedCover: GalleryPhoto | null = null;
|
||||
for (const p of all) {
|
||||
const isCover = p.id === input.id;
|
||||
const next: GalleryPhoto = { ...p, cover: isCover };
|
||||
await galleryPhotosKV.setItem(p.id, next);
|
||||
if (isCover) updatedCover = next;
|
||||
}
|
||||
return updatedCover;
|
||||
});
|
||||
|
||||
const deletePhoto = os
|
||||
.input(z.object({ sessionId: z.string(), id: z.string() }))
|
||||
.handler(async ({ input }) => {
|
||||
await assertOwner(input.sessionId);
|
||||
await galleryPhotosKV.removeItem(input.id);
|
||||
});
|
||||
|
||||
const updatePhotoOrder = os
|
||||
.input(
|
||||
z.object({
|
||||
sessionId: z.string(),
|
||||
photoOrders: z.array(z.object({ id: z.string(), order: z.number().int() })),
|
||||
})
|
||||
)
|
||||
.handler(async ({ input }) => {
|
||||
await assertOwner(input.sessionId);
|
||||
const updated: GalleryPhoto[] = [];
|
||||
for (const { id, order } of input.photoOrders) {
|
||||
const existing = await galleryPhotosKV.getItem(id);
|
||||
if (!existing) continue;
|
||||
const updatedPhoto: GalleryPhoto = { ...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({ sessionId: z.string() }))
|
||||
.handler(async ({ input }) => {
|
||||
await assertOwner(input.sessionId);
|
||||
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({ sessionId: z.string() }))
|
||||
.handler(async function* ({ input, signal }) {
|
||||
await assertOwner(input.sessionId);
|
||||
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,
|
||||
};
|
||||
|
||||
|
Reference in New Issue
Block a user