Nicholai a77f62f949 feat: implement CalDAV Nextcloud bidirectional calendar integration
Adds complete CalDAV integration for syncing appointments between the web app
and Nextcloud calendars with real-time availability checking and conflict resolution.

Core Features:
- Bidirectional sync: Web ↔ Nextcloud calendars
- Real-time availability checking with instant user feedback
- Conflict detection (Nextcloud is source of truth)
- Pending request workflow with 'REQUEST:' prefix for unconfirmed appointments
- Hard time blocking - any calendar event blocks booking slots
- Graceful degradation when CalDAV unavailable

New Dependencies:
- tsdav@^2.0.4 - TypeScript CalDAV client
- ical.js@^1.5.0 - iCalendar format parser/generator

Database Changes:
- New table: artist_calendars (stores calendar configuration per artist)
- New table: calendar_sync_logs (tracks all sync operations)
- Added caldav_uid and caldav_etag columns to appointments table
- Migration: sql/migrations/20250109_add_caldav_support.sql

New Services:
- lib/caldav-client.ts - Core CalDAV operations and iCalendar conversion
- lib/calendar-sync.ts - Bidirectional sync logic with error handling

New API Endpoints:
- GET /api/caldav/availability - Real-time availability checking
- POST /api/caldav/sync - Manual sync trigger (admin only)
- GET/POST/PUT/DELETE /api/admin/calendars - Calendar configuration CRUD

Updated Components:
- app/api/appointments/route.ts - Integrated CalDAV sync on CRUD operations
- components/booking-form.tsx - Added real-time availability indicator
- hooks/use-availability.ts - Custom hook for debounced availability checking

Documentation:
- docs/CALDAV-SETUP.md - Complete setup guide with troubleshooting
- docs/CALDAV-IMPLEMENTATION-SUMMARY.md - Technical implementation overview

Pending Tasks (for future PRs):
- Admin dashboard UI for calendar management
- Background sync worker (Cloudflare Workers cron)
- Unit and integration tests

Tested with local database migration and linting checks passed.
2025-10-08 20:44:17 -06:00

60 lines
1.8 KiB
TypeScript

import { z } from "zod"
const envSchema = z.object({
// Database
DATABASE_URL: z.string().url(),
DIRECT_URL: z.string().url().optional(),
// Authentication
NEXTAUTH_URL: z.string().url(),
NEXTAUTH_SECRET: z.string().min(1),
// OAuth Providers (optional)
GOOGLE_CLIENT_ID: z.string().optional(),
GOOGLE_CLIENT_SECRET: z.string().optional(),
GITHUB_CLIENT_ID: z.string().optional(),
GITHUB_CLIENT_SECRET: z.string().optional(),
// File Storage (AWS S3 or Cloudflare R2)
AWS_ACCESS_KEY_ID: z.string().min(1),
AWS_SECRET_ACCESS_KEY: z.string().min(1),
AWS_REGION: z.string().min(1),
AWS_BUCKET_NAME: z.string().min(1),
AWS_ENDPOINT_URL: z.string().url().optional(), // For Cloudflare R2
// Application
NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
// Optional: Email service
SMTP_HOST: z.string().optional(),
SMTP_PORT: z.string().optional(),
SMTP_USER: z.string().optional(),
SMTP_PASSWORD: z.string().optional(),
// Optional: Analytics
VERCEL_ANALYTICS_ID: z.string().optional(),
// CalDAV / Nextcloud Integration
NEXTCLOUD_BASE_URL: z.string().url().optional(),
NEXTCLOUD_USERNAME: z.string().optional(),
NEXTCLOUD_PASSWORD: z.string().optional(),
NEXTCLOUD_CALENDAR_BASE_PATH: z.string().default('/remote.php/dav/calendars'),
})
export type Env = z.infer<typeof envSchema>
// Validate environment variables at boot
function validateEnv(): Env {
try {
return envSchema.parse(process.env)
} catch (error) {
if (error instanceof z.ZodError) {
const missingVars = error.errors.map(err => err.path.join('.')).join(', ')
throw new Error(`Missing or invalid environment variables: ${missingVars}`)
}
throw error
}
}
export const env = validateEnv()