- Structured logging: replace log.* with log/slog JSON output across backend - Request logger middleware: logs method, path, status, duration for all non-health requests - Rate limiting: token bucket (5 req/min, burst 10) on AI endpoints (/api/ai/*) - Integration tests: full critical path test (auth -> create case -> add deadline -> dashboard) - Seed demo data: 1 tenant, 5 cases with deadlines/appointments/parties/events - docker-compose.yml: add all required env vars (DATABASE_URL, SUPABASE_*, ANTHROPIC_API_KEY) - .env.example: document all env vars including DATABASE_URL and CalDAV note
183 lines
6.3 KiB
Go
183 lines
6.3 KiB
Go
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) http.Handler {
|
|
mux := http.NewServeMux()
|
|
|
|
// Services
|
|
tenantSvc := services.NewTenantService(db)
|
|
caseSvc := services.NewCaseService(db)
|
|
partySvc := services.NewPartyService(db)
|
|
appointmentSvc := services.NewAppointmentService(db)
|
|
holidaySvc := services.NewHolidayService(db)
|
|
deadlineSvc := services.NewDeadlineService(db)
|
|
deadlineRuleSvc := services.NewDeadlineRuleService(db)
|
|
calculator := services.NewDeadlineCalculator(holidaySvc)
|
|
storageCli := services.NewStorageClient(cfg.SupabaseURL, cfg.SupabaseServiceKey)
|
|
documentSvc := services.NewDocumentService(db, storageCli)
|
|
|
|
// 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, db)
|
|
}
|
|
|
|
// Middleware
|
|
tenantResolver := auth.NewTenantResolver(tenantSvc)
|
|
|
|
dashboardSvc := services.NewDashboardService(db)
|
|
|
|
// Handlers
|
|
tenantH := handlers.NewTenantHandler(tenantSvc)
|
|
caseH := handlers.NewCaseHandler(caseSvc)
|
|
partyH := handlers.NewPartyHandler(partySvc)
|
|
apptH := handlers.NewAppointmentHandler(appointmentSvc)
|
|
deadlineH := handlers.NewDeadlineHandlers(deadlineSvc, db)
|
|
ruleH := handlers.NewDeadlineRuleHandlers(deadlineRuleSvc)
|
|
calcH := handlers.NewCalculateHandlers(calculator, deadlineRuleSvc)
|
|
dashboardH := handlers.NewDashboardHandler(dashboardSvc)
|
|
docH := handlers.NewDocumentHandler(documentSvc)
|
|
|
|
// 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("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)
|
|
|
|
// Tenant-scoped routes (require tenant context)
|
|
scoped := http.NewServeMux()
|
|
|
|
// Cases
|
|
scoped.HandleFunc("GET /api/cases", caseH.List)
|
|
scoped.HandleFunc("POST /api/cases", caseH.Create)
|
|
scoped.HandleFunc("GET /api/cases/{id}", caseH.Get)
|
|
scoped.HandleFunc("PUT /api/cases/{id}", caseH.Update)
|
|
scoped.HandleFunc("DELETE /api/cases/{id}", caseH.Delete)
|
|
|
|
// Parties
|
|
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
|
|
scoped.HandleFunc("GET /api/deadlines", deadlineH.ListAll)
|
|
scoped.HandleFunc("GET /api/cases/{caseID}/deadlines", deadlineH.ListForCase)
|
|
scoped.HandleFunc("POST /api/cases/{caseID}/deadlines", deadlineH.Create)
|
|
scoped.HandleFunc("PUT /api/deadlines/{deadlineID}", deadlineH.Update)
|
|
scoped.HandleFunc("PATCH /api/deadlines/{deadlineID}/complete", deadlineH.Complete)
|
|
scoped.HandleFunc("DELETE /api/deadlines/{deadlineID}", deadlineH.Delete)
|
|
|
|
// Deadline rules (reference data)
|
|
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
|
|
scoped.HandleFunc("POST /api/deadlines/calculate", calcH.Calculate)
|
|
|
|
// Appointments
|
|
scoped.HandleFunc("GET /api/appointments", apptH.List)
|
|
scoped.HandleFunc("POST /api/appointments", apptH.Create)
|
|
scoped.HandleFunc("PUT /api/appointments/{id}", apptH.Update)
|
|
scoped.HandleFunc("DELETE /api/appointments/{id}", apptH.Delete)
|
|
|
|
// Dashboard
|
|
scoped.HandleFunc("GET /api/dashboard", dashboardH.Get)
|
|
|
|
// Documents
|
|
scoped.HandleFunc("GET /api/cases/{id}/documents", docH.ListByCase)
|
|
scoped.HandleFunc("POST /api/cases/{id}/documents", 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)
|
|
|
|
// 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", aiLimiter.LimitFunc(aiH.ExtractDeadlines))
|
|
scoped.HandleFunc("POST /api/ai/summarize-case", aiLimiter.LimitFunc(aiH.SummarizeCase))
|
|
}
|
|
|
|
// CalDAV sync endpoints
|
|
if calDAVSvc != nil {
|
|
calDAVH := handlers.NewCalDAVHandler(calDAVSvc)
|
|
scoped.HandleFunc("POST /api/caldav/sync", 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))
|
|
|
|
return requestLogger(mux)
|
|
}
|
|
|
|
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", "error": err.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(),
|
|
)
|
|
})
|
|
}
|
|
|