package router import ( "encoding/json" "log/slog" "net/http" "time" "github.com/jmoiron/sqlx" "mgit.msbls.de/m/KanzlAI-mGMT/internal/auth" "mgit.msbls.de/m/KanzlAI-mGMT/internal/config" "mgit.msbls.de/m/KanzlAI-mGMT/internal/handlers" "mgit.msbls.de/m/KanzlAI-mGMT/internal/middleware" "mgit.msbls.de/m/KanzlAI-mGMT/internal/services" ) func New(db *sqlx.DB, authMW *auth.Middleware, cfg *config.Config, calDAVSvc *services.CalDAVService, notifSvc *services.NotificationService) http.Handler { mux := http.NewServeMux() // Services auditSvc := services.NewAuditService(db) tenantSvc := services.NewTenantService(db, auditSvc) caseSvc := services.NewCaseService(db, auditSvc) partySvc := services.NewPartyService(db, auditSvc) appointmentSvc := services.NewAppointmentService(db, auditSvc) holidaySvc := services.NewHolidayService(db) deadlineSvc := services.NewDeadlineService(db, auditSvc) deadlineRuleSvc := services.NewDeadlineRuleService(db) calculator := services.NewDeadlineCalculator(holidaySvc) storageCli := services.NewStorageClient(cfg.SupabaseURL, cfg.SupabaseServiceKey) documentSvc := services.NewDocumentService(db, storageCli, auditSvc) assignmentSvc := services.NewCaseAssignmentService(db) templateSvc := services.NewTemplateService(db, auditSvc) // AI service (optional — only if API key is configured) var aiH *handlers.AIHandler if cfg.AnthropicAPIKey != "" { aiSvc := services.NewAIService(cfg.AnthropicAPIKey, db) aiH = handlers.NewAIHandler(aiSvc) } // Middleware tenantResolver := auth.NewTenantResolver(tenantSvc) noteSvc := services.NewNoteService(db, auditSvc) dashboardSvc := services.NewDashboardService(db) // Notification handler (optional — nil in tests) var notifH *handlers.NotificationHandler if notifSvc != nil { notifH = handlers.NewNotificationHandler(notifSvc, db) } // Handlers auditH := handlers.NewAuditLogHandler(auditSvc) tenantH := handlers.NewTenantHandler(tenantSvc) caseH := handlers.NewCaseHandler(caseSvc) partyH := handlers.NewPartyHandler(partySvc) apptH := handlers.NewAppointmentHandler(appointmentSvc) deadlineH := handlers.NewDeadlineHandlers(deadlineSvc) ruleH := handlers.NewDeadlineRuleHandlers(deadlineRuleSvc) calcH := handlers.NewCalculateHandlers(calculator, deadlineRuleSvc) dashboardH := handlers.NewDashboardHandler(dashboardSvc) noteH := handlers.NewNoteHandler(noteSvc) eventH := handlers.NewCaseEventHandler(db) docH := handlers.NewDocumentHandler(documentSvc) assignmentH := handlers.NewCaseAssignmentHandler(assignmentSvc) templateH := handlers.NewTemplateHandler(templateSvc, caseSvc, partySvc, deadlineSvc, tenantSvc) // Public routes mux.HandleFunc("GET /health", handleHealth(db)) // Authenticated API routes api := http.NewServeMux() // Tenant management (no tenant resolver — these operate across tenants) api.HandleFunc("POST /api/tenants", tenantH.CreateTenant) api.HandleFunc("GET /api/tenants", tenantH.ListTenants) api.HandleFunc("GET /api/tenants/{id}", tenantH.GetTenant) api.HandleFunc("PUT /api/tenants/{id}/settings", tenantH.UpdateSettings) api.HandleFunc("POST /api/tenants/{id}/invite", tenantH.InviteUser) api.HandleFunc("DELETE /api/tenants/{id}/members/{uid}", tenantH.RemoveMember) api.HandleFunc("GET /api/tenants/{id}/members", tenantH.ListMembers) api.HandleFunc("PUT /api/tenants/{id}/members/{uid}/role", tenantH.UpdateMemberRole) // Permission-wrapping helper: wraps a HandlerFunc with a permission check perm := func(p auth.Permission, fn http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { role := auth.UserRoleFromContext(r.Context()) if !auth.HasPermission(role, p) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusForbidden) w.Write([]byte(`{"error":"insufficient permissions"}`)) return } fn(w, r) } } // Tenant-scoped routes (require tenant context) scoped := http.NewServeMux() // Current user info (role, permissions) — all authenticated users scoped.HandleFunc("GET /api/me", tenantH.GetMe) // Cases — all can view, create needs PermCreateCase, archive needs PermCreateCase scoped.HandleFunc("GET /api/cases", caseH.List) scoped.HandleFunc("POST /api/cases", perm(auth.PermCreateCase, caseH.Create)) scoped.HandleFunc("GET /api/cases/{id}", caseH.Get) scoped.HandleFunc("PUT /api/cases/{id}", caseH.Update) scoped.HandleFunc("DELETE /api/cases/{id}", perm(auth.PermCreateCase, caseH.Delete)) // Parties — same access as case editing scoped.HandleFunc("GET /api/cases/{id}/parties", partyH.List) scoped.HandleFunc("POST /api/cases/{id}/parties", partyH.Create) scoped.HandleFunc("PUT /api/parties/{partyId}", partyH.Update) scoped.HandleFunc("DELETE /api/parties/{partyId}", partyH.Delete) // Deadlines — manage needs PermManageDeadlines, view is open scoped.HandleFunc("GET /api/deadlines/{deadlineID}", deadlineH.Get) scoped.HandleFunc("GET /api/deadlines", deadlineH.ListAll) scoped.HandleFunc("GET /api/cases/{caseID}/deadlines", deadlineH.ListForCase) scoped.HandleFunc("POST /api/cases/{caseID}/deadlines", perm(auth.PermManageDeadlines, deadlineH.Create)) scoped.HandleFunc("PUT /api/deadlines/{deadlineID}", perm(auth.PermManageDeadlines, deadlineH.Update)) scoped.HandleFunc("PATCH /api/deadlines/{deadlineID}/complete", perm(auth.PermManageDeadlines, deadlineH.Complete)) scoped.HandleFunc("DELETE /api/deadlines/{deadlineID}", perm(auth.PermManageDeadlines, deadlineH.Delete)) // Deadline rules (reference data) — all can read scoped.HandleFunc("GET /api/deadline-rules", ruleH.List) scoped.HandleFunc("GET /api/deadline-rules/{type}", ruleH.GetRuleTree) scoped.HandleFunc("GET /api/proceeding-types", ruleH.ListProceedingTypes) // Deadline calculator — all can use scoped.HandleFunc("POST /api/deadlines/calculate", calcH.Calculate) // Appointments — all can manage scoped.HandleFunc("GET /api/appointments/{id}", apptH.Get) scoped.HandleFunc("GET /api/appointments", apptH.List) scoped.HandleFunc("POST /api/appointments", perm(auth.PermManageAppointments, apptH.Create)) scoped.HandleFunc("PUT /api/appointments/{id}", perm(auth.PermManageAppointments, apptH.Update)) scoped.HandleFunc("DELETE /api/appointments/{id}", perm(auth.PermManageAppointments, apptH.Delete)) // Case assignments — manage team required for assign/unassign scoped.HandleFunc("GET /api/cases/{id}/assignments", assignmentH.List) scoped.HandleFunc("POST /api/cases/{id}/assignments", perm(auth.PermManageTeam, assignmentH.Assign)) scoped.HandleFunc("DELETE /api/cases/{id}/assignments/{uid}", perm(auth.PermManageTeam, assignmentH.Unassign)) // Case events — all can view scoped.HandleFunc("GET /api/case-events/{id}", eventH.Get) // Notes — all can manage scoped.HandleFunc("GET /api/notes", noteH.List) scoped.HandleFunc("POST /api/notes", noteH.Create) scoped.HandleFunc("PUT /api/notes/{id}", noteH.Update) scoped.HandleFunc("DELETE /api/notes/{id}", noteH.Delete) // Dashboard — all can view scoped.HandleFunc("GET /api/dashboard", dashboardH.Get) // Audit log scoped.HandleFunc("GET /api/audit-log", auditH.List) // Documents — all can upload, delete checked in handler scoped.HandleFunc("GET /api/cases/{id}/documents", docH.ListByCase) scoped.HandleFunc("POST /api/cases/{id}/documents", perm(auth.PermUploadDocuments, docH.Upload)) scoped.HandleFunc("GET /api/documents/{docId}", docH.Download) scoped.HandleFunc("GET /api/documents/{docId}/meta", docH.GetMeta) scoped.HandleFunc("DELETE /api/documents/{docId}", docH.Delete) // Document templates — all can view, create/edit needs PermCreateCase scoped.HandleFunc("GET /api/templates", templateH.List) scoped.HandleFunc("GET /api/templates/{id}", templateH.Get) scoped.HandleFunc("POST /api/templates", perm(auth.PermCreateCase, templateH.Create)) scoped.HandleFunc("PUT /api/templates/{id}", perm(auth.PermCreateCase, templateH.Update)) scoped.HandleFunc("DELETE /api/templates/{id}", perm(auth.PermCreateCase, templateH.Delete)) scoped.HandleFunc("POST /api/templates/{id}/render", templateH.Render) // AI endpoints (rate limited: 5 req/min burst 10 per IP) if aiH != nil { aiLimiter := middleware.NewTokenBucket(5.0/60.0, 10) scoped.HandleFunc("POST /api/ai/extract-deadlines", perm(auth.PermAIExtraction, aiLimiter.LimitFunc(aiH.ExtractDeadlines))) scoped.HandleFunc("POST /api/ai/summarize-case", perm(auth.PermAIExtraction, aiLimiter.LimitFunc(aiH.SummarizeCase))) } // Notifications if notifH != nil { scoped.HandleFunc("GET /api/notifications", notifH.List) scoped.HandleFunc("GET /api/notifications/unread-count", notifH.UnreadCount) scoped.HandleFunc("PATCH /api/notifications/{id}/read", notifH.MarkRead) scoped.HandleFunc("PATCH /api/notifications/read-all", notifH.MarkAllRead) scoped.HandleFunc("GET /api/notification-preferences", notifH.GetPreferences) scoped.HandleFunc("PUT /api/notification-preferences", notifH.UpdatePreferences) } // CalDAV sync endpoints if calDAVSvc != nil { calDAVH := handlers.NewCalDAVHandler(calDAVSvc) scoped.HandleFunc("POST /api/caldav/sync", perm(auth.PermManageSettings, calDAVH.TriggerSync)) scoped.HandleFunc("GET /api/caldav/status", calDAVH.GetStatus) } // Wire: auth -> tenant routes go directly, scoped routes get tenant resolver api.Handle("/api/", tenantResolver.Resolve(scoped)) mux.Handle("/api/", authMW.RequireAuth(api)) // Apply security middleware stack: CORS -> Security Headers -> Request Logger -> Routes var handler http.Handler = mux handler = requestLogger(handler) handler = middleware.SecurityHeaders(handler) handler = middleware.CORS(cfg.FrontendOrigin)(handler) return handler } func handleHealth(db *sqlx.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if err := db.Ping(); err != nil { w.WriteHeader(http.StatusServiceUnavailable) json.NewEncoder(w).Encode(map[string]string{"status": "error"}) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{"status": "ok"}) } } type statusWriter struct { http.ResponseWriter status int } func (w *statusWriter) WriteHeader(code int) { w.status = code w.ResponseWriter.WriteHeader(code) } func requestLogger(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Skip health checks to reduce noise if r.URL.Path == "/health" { next.ServeHTTP(w, r) return } sw := &statusWriter{ResponseWriter: w, status: http.StatusOK} start := time.Now() next.ServeHTTP(sw, r) slog.Info("request", "method", r.Method, "path", r.URL.Path, "status", sw.status, "duration_ms", time.Since(start).Milliseconds(), ) }) }