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);
});