diff --git a/backend/internal/auth/context.go b/backend/internal/auth/context.go index c2aeaef..9553104 100644 --- a/backend/internal/auth/context.go +++ b/backend/internal/auth/context.go @@ -9,19 +9,11 @@ import ( type contextKey string const ( -<<<<<<< HEAD userIDKey contextKey = "user_id" tenantIDKey contextKey = "tenant_id" + userRoleKey contextKey = "user_role" ipKey contextKey = "ip_address" userAgentKey contextKey = "user_agent" -||||||| 82878df - userIDKey contextKey = "user_id" - tenantIDKey contextKey = "tenant_id" -======= - userIDKey contextKey = "user_id" - tenantIDKey contextKey = "tenant_id" - userRoleKey contextKey = "user_role" ->>>>>>> mai/pike/p0-role-based ) func ContextWithUserID(ctx context.Context, userID uuid.UUID) context.Context { @@ -41,7 +33,15 @@ func TenantFromContext(ctx context.Context) (uuid.UUID, bool) { id, ok := ctx.Value(tenantIDKey).(uuid.UUID) return id, ok } -<<<<<<< HEAD + +func ContextWithUserRole(ctx context.Context, role string) context.Context { + return context.WithValue(ctx, userRoleKey, role) +} + +func UserRoleFromContext(ctx context.Context) string { + role, _ := ctx.Value(userRoleKey).(string) + return role +} func ContextWithRequestInfo(ctx context.Context, ip, userAgent string) context.Context { ctx = context.WithValue(ctx, ipKey, ip) @@ -62,15 +62,3 @@ func UserAgentFromContext(ctx context.Context) *string { } return nil } -||||||| 82878df -======= - -func ContextWithUserRole(ctx context.Context, role string) context.Context { - return context.WithValue(ctx, userRoleKey, role) -} - -func UserRoleFromContext(ctx context.Context) string { - role, _ := ctx.Value(userRoleKey).(string) - return role -} ->>>>>>> mai/pike/p0-role-based diff --git a/backend/internal/auth/middleware.go b/backend/internal/auth/middleware.go index 02428c6..2a45942 100644 --- a/backend/internal/auth/middleware.go +++ b/backend/internal/auth/middleware.go @@ -35,36 +35,6 @@ func (m *Middleware) RequireAuth(next http.Handler) http.Handler { } ctx := ContextWithUserID(r.Context(), userID) -<<<<<<< HEAD - // Tenant resolution is handled by TenantResolver middleware for scoped routes. - // Tenant management routes handle their own access control. -||||||| 82878df - - // Resolve tenant and role from user_tenants - var membership struct { - TenantID uuid.UUID `db:"tenant_id"` - Role string `db:"role"` - } - err = m.db.GetContext(r.Context(), &membership, - "SELECT tenant_id, role FROM user_tenants WHERE user_id = $1 LIMIT 1", userID) - if err != nil { - http.Error(w, "no tenant found for user", http.StatusForbidden) - return - } - ctx = ContextWithTenantID(ctx, membership.TenantID) - ctx = ContextWithUserRole(ctx, membership.Role) - -======= - - // Resolve tenant from user_tenants - var tenantID uuid.UUID - err = m.db.GetContext(r.Context(), &tenantID, - "SELECT tenant_id FROM user_tenants WHERE user_id = $1 LIMIT 1", userID) - if err != nil { - http.Error(w, "no tenant found for user", http.StatusForbidden) - return - } - ctx = ContextWithTenantID(ctx, tenantID) // Capture IP and user-agent for audit logging ip := r.Header.Get("X-Forwarded-For") @@ -73,7 +43,8 @@ func (m *Middleware) RequireAuth(next http.Handler) http.Handler { } ctx = ContextWithRequestInfo(ctx, ip, r.UserAgent()) ->>>>>>> mai/knuth/p0-audit-trail-append + // Tenant resolution is handled by TenantResolver middleware for scoped routes. + // Tenant management routes handle their own access control. next.ServeHTTP(w, r.WithContext(ctx)) }) } diff --git a/backend/internal/auth/tenant_resolver.go b/backend/internal/auth/tenant_resolver.go index 0dab57f..c4b883a 100644 --- a/backend/internal/auth/tenant_resolver.go +++ b/backend/internal/auth/tenant_resolver.go @@ -12,12 +12,8 @@ import ( // Defined as an interface to avoid circular dependency with services. type TenantLookup interface { FirstTenantForUser(ctx context.Context, userID uuid.UUID) (*uuid.UUID, error) -<<<<<<< HEAD VerifyAccess(ctx context.Context, userID, tenantID uuid.UUID) (bool, error) -||||||| 82878df -======= GetUserRole(ctx context.Context, userID, tenantID uuid.UUID) (string, error) ->>>>>>> mai/pike/p0-role-based } // TenantResolver is middleware that resolves the tenant from X-Tenant-ID header @@ -46,35 +42,20 @@ func (tr *TenantResolver) Resolve(next http.Handler) http.Handler { http.Error(w, `{"error":"invalid X-Tenant-ID"}`, http.StatusBadRequest) return } -<<<<<<< HEAD - // Verify user has access to this tenant - hasAccess, err := tr.lookup.VerifyAccess(r.Context(), userID, parsed) + // Verify user has access and get their role + role, err := tr.lookup.GetUserRole(r.Context(), userID, parsed) if err != nil { slog.Error("tenant access check failed", "error", err, "user_id", userID, "tenant_id", parsed) http.Error(w, `{"error":"internal error"}`, http.StatusInternalServerError) return } - if !hasAccess { + if role == "" { http.Error(w, `{"error":"no access to tenant"}`, http.StatusForbidden) return } -||||||| 82878df -======= - // Verify user has access and get their role - role, err := tr.lookup.GetUserRole(r.Context(), userID, parsed) - if err != nil { - http.Error(w, "error checking tenant access", http.StatusInternalServerError) - return - } - if role == "" { - http.Error(w, "no access to this tenant", http.StatusForbidden) - return - } ->>>>>>> mai/pike/p0-role-based tenantID = parsed - // Override the role from middleware with the correct one for this tenant r = r.WithContext(ContextWithUserRole(r.Context(), role)) } else { // Default to user's first tenant (role already set by middleware) diff --git a/backend/internal/auth/tenant_resolver_test.go b/backend/internal/auth/tenant_resolver_test.go index d0300c2..cdb210a 100644 --- a/backend/internal/auth/tenant_resolver_test.go +++ b/backend/internal/auth/tenant_resolver_test.go @@ -10,49 +10,35 @@ import ( ) type mockTenantLookup struct { -<<<<<<< HEAD tenantID *uuid.UUID err error hasAccess bool accessErr error -||||||| 82878df - tenantID *uuid.UUID - err error -======= - tenantID *uuid.UUID - role string - err error ->>>>>>> mai/pike/p0-role-based + role string + noAccess bool // when true, GetUserRole returns "" } func (m *mockTenantLookup) FirstTenantForUser(ctx context.Context, userID uuid.UUID) (*uuid.UUID, error) { return m.tenantID, m.err } -<<<<<<< HEAD func (m *mockTenantLookup) VerifyAccess(ctx context.Context, userID, tenantID uuid.UUID) (bool, error) { return m.hasAccess, m.accessErr } -||||||| 82878df -======= func (m *mockTenantLookup) GetUserRole(ctx context.Context, userID, tenantID uuid.UUID) (string, error) { + if m.noAccess { + return "", m.err + } if m.role != "" { return m.role, m.err } return "associate", m.err } ->>>>>>> mai/pike/p0-role-based func TestTenantResolver_FromHeader(t *testing.T) { tenantID := uuid.New() -<<<<<<< HEAD - tr := NewTenantResolver(&mockTenantLookup{hasAccess: true}) -||||||| 82878df - tr := NewTenantResolver(&mockTenantLookup{}) -======= - tr := NewTenantResolver(&mockTenantLookup{role: "partner"}) ->>>>>>> mai/pike/p0-role-based + tr := NewTenantResolver(&mockTenantLookup{role: "partner", hasAccess: true}) var gotTenantID uuid.UUID next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -81,7 +67,7 @@ func TestTenantResolver_FromHeader(t *testing.T) { func TestTenantResolver_FromHeader_NoAccess(t *testing.T) { tenantID := uuid.New() - tr := NewTenantResolver(&mockTenantLookup{hasAccess: false}) + tr := NewTenantResolver(&mockTenantLookup{noAccess: true}) next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { t.Fatal("next should not be called") diff --git a/backend/internal/handlers/deadlines.go b/backend/internal/handlers/deadlines.go index e1a2a39..1ff7629 100644 --- a/backend/internal/handlers/deadlines.go +++ b/backend/internal/handlers/deadlines.go @@ -198,18 +198,8 @@ func (h *DeadlineHandlers) Delete(w http.ResponseWriter, r *http.Request) { return } -<<<<<<< HEAD - if err := h.deadlines.Delete(tenantID, deadlineID); err != nil { + if err := h.deadlines.Delete(r.Context(), tenantID, deadlineID); err != nil { writeError(w, http.StatusNotFound, "deadline not found") -||||||| 82878df - err = h.deadlines.Delete(tenantID, deadlineID) - if err != nil { - writeError(w, http.StatusNotFound, err.Error()) -======= - err = h.deadlines.Delete(r.Context(), tenantID, deadlineID) - if err != nil { - writeError(w, http.StatusNotFound, err.Error()) ->>>>>>> mai/knuth/p0-audit-trail-append return } diff --git a/backend/internal/router/router.go b/backend/internal/router/router.go index 84b7f8e..d64c403 100644 --- a/backend/internal/router/router.go +++ b/backend/internal/router/router.go @@ -15,7 +15,7 @@ import ( "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 { +func New(db *sqlx.DB, authMW *auth.Middleware, cfg *config.Config, calDAVSvc *services.CalDAVService, notifSvc *services.NotificationService, youpcDB ...*sqlx.DB) http.Handler { mux := http.NewServeMux() // Services @@ -29,19 +29,17 @@ func New(db *sqlx.DB, authMW *auth.Middleware, cfg *config.Config, calDAVSvc *se deadlineRuleSvc := services.NewDeadlineRuleService(db) calculator := services.NewDeadlineCalculator(holidaySvc) storageCli := services.NewStorageClient(cfg.SupabaseURL, cfg.SupabaseServiceKey) -<<<<<<< HEAD documentSvc := services.NewDocumentService(db, storageCli, auditSvc) -||||||| 82878df - documentSvc := services.NewDocumentService(db, storageCli) -======= - documentSvc := services.NewDocumentService(db, storageCli) assignmentSvc := services.NewCaseAssignmentService(db) ->>>>>>> mai/pike/p0-role-based // AI service (optional — only if API key is configured) var aiH *handlers.AIHandler if cfg.AnthropicAPIKey != "" { - aiSvc := services.NewAIService(cfg.AnthropicAPIKey, db) + var ydb *sqlx.DB + if len(youpcDB) > 0 { + ydb = youpcDB[0] + } + aiSvc := services.NewAIService(cfg.AnthropicAPIKey, db, ydb) aiH = handlers.NewAIHandler(aiSvc) } @@ -162,16 +160,10 @@ func New(db *sqlx.DB, authMW *auth.Middleware, cfg *config.Config, calDAVSvc *se // Dashboard — all can view scoped.HandleFunc("GET /api/dashboard", dashboardH.Get) -<<<<<<< HEAD // Audit log scoped.HandleFunc("GET /api/audit-log", auditH.List) - // Documents -||||||| 82878df - // Documents -======= // Documents — all can upload, delete checked in handler (own vs all) ->>>>>>> mai/pike/p0-role-based 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) @@ -183,9 +175,11 @@ func New(db *sqlx.DB, authMW *auth.Middleware, cfg *config.Config, calDAVSvc *se 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))) + scoped.HandleFunc("POST /api/ai/draft-document", perm(auth.PermAIExtraction, aiLimiter.LimitFunc(aiH.DraftDocument))) + scoped.HandleFunc("POST /api/ai/case-strategy", perm(auth.PermAIExtraction, aiLimiter.LimitFunc(aiH.CaseStrategy))) + scoped.HandleFunc("POST /api/ai/similar-cases", perm(auth.PermAIExtraction, aiLimiter.LimitFunc(aiH.SimilarCases))) } -<<<<<<< HEAD // Notifications if notifH != nil { scoped.HandleFunc("GET /api/notifications", notifH.List) @@ -196,12 +190,7 @@ func New(db *sqlx.DB, authMW *auth.Middleware, cfg *config.Config, calDAVSvc *se scoped.HandleFunc("PUT /api/notification-preferences", notifH.UpdatePreferences) } - // CalDAV sync endpoints -||||||| 82878df - // CalDAV sync endpoints -======= // CalDAV sync endpoints — settings permission required ->>>>>>> mai/pike/p0-role-based if calDAVSvc != nil { calDAVH := handlers.NewCalDAVHandler(calDAVSvc) scoped.HandleFunc("POST /api/caldav/sync", perm(auth.PermManageSettings, calDAVH.TriggerSync))