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:
2025-10-06 12:41:50 +02:00
parent 244eeee142
commit fbfdceeee6
28 changed files with 3584 additions and 0 deletions

131
server-dist/rpc/gallery.js Normal file
View 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,
};