feat: add CalDAV bidirectional sync service (Phase 3O)
Implements CalDAV sync using github.com/emersion/go-webdav: - CalDAVService with background polling (configurable per-tenant interval) - Push: deadlines -> VTODO, appointments -> VEVENT on create/update/delete - Pull: periodic fetch from CalDAV, reconcile with local DB - Conflict resolution: KanzlAI wins dates/status, CalDAV wins notes/description - Conflicts logged as case_events with caldav_conflict type - UID pattern: kanzlai-{deadline|appointment}-{uuid}@kanzlai.msbls.de - CalDAV config per tenant in tenants.settings JSONB Endpoints: - POST /api/caldav/sync — trigger full sync for current tenant - GET /api/caldav/status — last sync time, item counts, errors 8 unit tests for UID generation, parsing, path construction, config parsing.
This commit is contained in:
68
backend/internal/handlers/caldav.go
Normal file
68
backend/internal/handlers/caldav.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"mgit.msbls.de/m/KanzlAI-mGMT/internal/auth"
|
||||
"mgit.msbls.de/m/KanzlAI-mGMT/internal/services"
|
||||
)
|
||||
|
||||
// CalDAVHandler handles CalDAV sync HTTP endpoints.
|
||||
type CalDAVHandler struct {
|
||||
svc *services.CalDAVService
|
||||
}
|
||||
|
||||
// NewCalDAVHandler creates a new CalDAV handler.
|
||||
func NewCalDAVHandler(svc *services.CalDAVService) *CalDAVHandler {
|
||||
return &CalDAVHandler{svc: svc}
|
||||
}
|
||||
|
||||
// TriggerSync handles POST /api/caldav/sync — triggers a full sync for the current tenant.
|
||||
func (h *CalDAVHandler) TriggerSync(w http.ResponseWriter, r *http.Request) {
|
||||
tenantID, ok := auth.TenantFromContext(r.Context())
|
||||
if !ok {
|
||||
writeError(w, http.StatusUnauthorized, "no tenant context")
|
||||
return
|
||||
}
|
||||
|
||||
cfg, err := h.svc.LoadTenantConfig(tenantID)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
status, err := h.svc.SyncTenant(r.Context(), tenantID, *cfg)
|
||||
if err != nil {
|
||||
// Still return the status — it contains partial results + error info
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"status": "completed_with_errors",
|
||||
"sync": status,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"status": "ok",
|
||||
"sync": status,
|
||||
})
|
||||
}
|
||||
|
||||
// GetStatus handles GET /api/caldav/status — returns last sync status.
|
||||
func (h *CalDAVHandler) GetStatus(w http.ResponseWriter, r *http.Request) {
|
||||
tenantID, ok := auth.TenantFromContext(r.Context())
|
||||
if !ok {
|
||||
writeError(w, http.StatusUnauthorized, "no tenant context")
|
||||
return
|
||||
}
|
||||
|
||||
status := h.svc.GetStatus(tenantID)
|
||||
if status == nil {
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
"status": "no_sync_yet",
|
||||
"last_sync_at": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, status)
|
||||
}
|
||||
Reference in New Issue
Block a user