feat: CalDAV-Integration für Admin-Kalender
- Neue CalDAV-Route mit PROPFIND und GET-Endpoints - ICS-Format-Generator für Buchungsdaten - Token-basierte Authentifizierung für CalDAV-Zugriff - Admin-Interface mit CalDAV-Link-Generator - Schritt-für-Schritt-Anleitung für Kalender-Apps - 24h-Token-Ablaufzeit für Sicherheit - Unterstützung für Outlook, Google Calendar, Apple Calendar, Thunderbird Fixes: Admin kann jetzt Terminkalender in externen Apps abonnieren
This commit is contained in:
131
server-dist/rpc/gallery.js
Normal file
131
server-dist/rpc/gallery.js
Normal file
@@ -0,0 +1,131 @@
|
||||
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),
|
||||
});
|
||||
// KV Storage
|
||||
const galleryPhotosKV = createKV("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 = {
|
||||
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 = 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({ 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 = [];
|
||||
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({ 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