import { Hono } from "hono"; import { createKV } from "../lib/create-kv.js"; // KV-Stores const bookingsKV = createKV("bookings"); const treatmentsKV = createKV("treatments"); const sessionsKV = createKV("sessions"); export const caldavApp = new Hono(); // Helper-Funktionen für ICS-Format function formatDateTime(dateStr, timeStr) { // Konvertiere YYYY-MM-DD HH:MM zu UTC-Format für ICS const [year, month, day] = dateStr.split('-').map(Number); const [hours, minutes] = timeStr.split(':').map(Number); const date = new Date(year, month - 1, day, hours, minutes); return date.toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, ''); } function generateICSContent(bookings, treatments) { const now = new Date().toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, ''); let ics = `BEGIN:VCALENDAR VERSION:2.0 PRODID:-//Stargirlnails//Booking Calendar//DE CALSCALE:GREGORIAN METHOD:PUBLISH X-WR-CALNAME:Stargirlnails Termine X-WR-CALDESC:Terminkalender für Stargirlnails X-WR-TIMEZONE:Europe/Berlin `; // Nur bestätigte und ausstehende Termine in den Kalender aufnehmen const activeBookings = bookings.filter(b => b.status === 'confirmed' || b.status === 'pending'); for (const booking of activeBookings) { const treatment = treatments.find(t => t.id === booking.treatmentId); const treatmentName = treatment?.name || 'Unbekannte Behandlung'; const duration = booking.bookedDurationMinutes || treatment?.duration || 60; const startTime = formatDateTime(booking.appointmentDate, booking.appointmentTime); const endTime = formatDateTime(booking.appointmentDate, `${String(Math.floor((parseInt(booking.appointmentTime.split(':')[0]) * 60 + parseInt(booking.appointmentTime.split(':')[1]) + duration) / 60)).padStart(2, '0')}:${String((parseInt(booking.appointmentTime.split(':')[0]) * 60 + parseInt(booking.appointmentTime.split(':')[1]) + duration) % 60).padStart(2, '0')}`); // UID für jeden Termin (eindeutig) const uid = `booking-${booking.id}@stargirlnails.de`; // Status für ICS const status = booking.status === 'confirmed' ? 'CONFIRMED' : 'TENTATIVE'; ics += `BEGIN:VEVENT UID:${uid} DTSTAMP:${now} DTSTART:${startTime} DTEND:${endTime} SUMMARY:${treatmentName} - ${booking.customerName} DESCRIPTION:Behandlung: ${treatmentName}\\nKunde: ${booking.customerName}${booking.customerPhone ? `\\nTelefon: ${booking.customerPhone}` : ''}${booking.notes ? `\\nNotizen: ${booking.notes}` : ''} STATUS:${status} TRANSP:OPAQUE END:VEVENT `; } ics += `END:VCALENDAR`; return ics; } // CalDAV Discovery (PROPFIND auf Root) caldavApp.all("/", async (c) => { if (c.req.method !== 'PROPFIND') { return c.text('Method Not Allowed', 405); } const response = ` /caldav/ Stargirlnails Terminkalender Termine für Stargirlnails Europe/Berlin HTTP/1.1 200 OK `; return c.text(response, 207, { "Content-Type": "application/xml; charset=utf-8", "DAV": "1, 3, calendar-access, calendar-schedule", }); }); // Calendar Collection PROPFIND caldavApp.all("/calendar/", async (c) => { if (c.req.method !== 'PROPFIND') { return c.text('Method Not Allowed', 405); } const response = ` /caldav/calendar/ Stargirlnails Termine Alle Termine von Stargirlnails Europe/Berlin ${Date.now()} ${Date.now()} HTTP/1.1 200 OK `; return c.text(response, 207, { "Content-Type": "application/xml; charset=utf-8", }); }); // Calendar Events PROPFIND caldavApp.all("/calendar/events.ics", async (c) => { if (c.req.method !== 'PROPFIND') { return c.text('Method Not Allowed', 405); } const response = ` /caldav/calendar/events.ics text/calendar; charset=utf-8 "${Date.now()}" Stargirlnails Termine BEGIN:VCALENDAR\\nVERSION:2.0\\nEND:VCALENDAR HTTP/1.1 200 OK `; return c.text(response, 207, { "Content-Type": "application/xml; charset=utf-8", }); }); // GET Calendar Data (ICS-Datei) caldavApp.get("/calendar/events.ics", async (c) => { try { // Authentifizierung über Token im Query-Parameter const token = c.req.query('token'); if (!token) { return c.text('Unauthorized - Token required', 401); } // Token validieren const tokenData = await sessionsKV.getItem(token); if (!tokenData) { return c.text('Unauthorized - Invalid token', 401); } // Prüfe, ob es ein CalDAV-Token ist (durch Ablaufzeit und fehlende type-Eigenschaft erkennbar) // CalDAV-Tokens haben eine kürzere Ablaufzeit (24h) als normale Sessions const tokenAge = Date.now() - new Date(tokenData.createdAt).getTime(); if (tokenAge > 24 * 60 * 60 * 1000) { // 24 Stunden return c.text('Unauthorized - Token expired', 401); } // Token-Ablaufzeit prüfen if (new Date(tokenData.expiresAt) < new Date()) { return c.text('Unauthorized - Token expired', 401); } const bookings = await bookingsKV.getAllItems(); const treatments = await treatmentsKV.getAllItems(); const icsContent = generateICSContent(bookings, treatments); return c.text(icsContent, 200, { "Content-Type": "text/calendar; charset=utf-8", "Content-Disposition": "inline; filename=\"stargirlnails-termine.ics\"", "Cache-Control": "no-cache, no-store, must-revalidate", "Pragma": "no-cache", "Expires": "0", }); } catch (error) { console.error("CalDAV GET error:", error); return c.text('Internal Server Error', 500); } }); // Fallback für andere CalDAV-Requests caldavApp.all("*", async (c) => { console.log(`CalDAV: Unhandled ${c.req.method} request to ${c.req.url}`); return c.text('Not Found', 404); });