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.
60 lines
1.8 KiB
TypeScript
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()
|