Files
dezky/apps/booking/server/utils/forward.ts
T
Ronni Baslund 5ed3d2bc5f feat(scheduling): dezky Scheduling — Calendly-style booking on Stalwart calendars
First-party booking system on top of Stalwart calendars (no third-party
scheduling dependency). Hosts expose public booking pages; visitors pick a
slot computed from the host's live Stalwart free/busy, and confirming writes
the event to the host's calendar and sends a dezky-branded confirmation with
an .ics.

platform-api (services/platform-api/src/scheduling):
- Schemas: Host, StalwartCredential (AES-256-GCM at rest), AvailabilitySchedule,
  EventType, Booking, SlotLock (unique (hostId,startUtc) + TTL).
- StalwartCalendarModule: JMAP gateway (free/busy via Principal/getAvailability,
  event create/delete, scheduleAgent=client) + on-behalf app-password
  provisioning. CredentialCipher for at-rest encryption.
- DST-correct slot engine (Luxon) with unit tests; two-layer double-booking
  guard (atomic SlotLock + live free/busy re-check).
- Booking confirm/cancel/reschedule, branded email + .ics via JMAP submission,
  self-service manage tokens. /api/v1 public + tenant-gated admin routes,
  per-IP rate limiting.

apps/booking: standalone public, whitelabel booking app (booking.dezky.eu) —
path-based tenant resolution, per-tenant brand colour, booking + manage flows.

apps/portal: admin scheduling page (hosts, event types, availability, bookings
with edit/delete + admin cancel/reschedule) and proxy routes.

infra: booking dev service in docker-compose; scheduling env vars.
2026-06-07 00:17:36 +02:00

24 lines
948 B
TypeScript

import type { H3Event } from 'h3'
// Forward a request to platform-api's public scheduling API, preserving upstream
// status codes + messages so the client sees a real 404/409/503 instead of a
// generic 500. The internal API hostname never reaches the browser.
export async function forward(
_event: H3Event,
method: 'GET' | 'POST',
path: string,
opts: { query?: Record<string, any>; body?: any } = {},
): Promise<any> {
const base = useRuntimeConfig().platformApiUrl
try {
return await $fetch(base + path, { method, query: opts.query, body: opts.body })
} catch (err: any) {
const status = err?.response?.status ?? 502
const raw = err?.data?.message ?? err?.response?._data?.message ?? 'Upstream error'
throw createError({ statusCode: status, statusMessage: Array.isArray(raw) ? raw.join(', ') : String(raw) })
}
}
// Encode a single path segment safely.
export const seg = (s: string) => encodeURIComponent(s)