Backend: - auth/permissions.go: full permission matrix with RequirePermission/RequireRole middleware, CanEditCase, CanDeleteDocument helpers - auth/context.go: add user role to request context - auth/middleware.go: resolve role alongside tenant in auth flow - auth/tenant_resolver.go: verify membership + resolve role for X-Tenant-ID - handlers/case_assignments.go: CRUD for case-level user assignments - handlers/tenant_handler.go: UpdateMemberRole, GetMe (/api/me) endpoints - handlers/documents.go: permission-based delete (own vs all) - router/router.go: permission-wrapped routes for all endpoints - services/case_assignment_service.go: assign/unassign with tenant validation - services/tenant_service.go: UpdateMemberRole with owner protection - models/case_assignment.go: CaseAssignment model Database: - user_tenants.role: CHECK constraint (owner/partner/associate/paralegal/secretary) - case_assignments table: case_id, user_id, role (lead/team/viewer) - Migrated existing admin->partner, member->associate Frontend: - usePermissions hook: fetches /api/me, provides can() helper - TeamSettings: 5-role dropdown, role change, permission-gated invite - CaseAssignments: new component for case-level team management - Sidebar: conditionally hides AI/Settings based on permissions - Cases page: hides "Neue Akte" button for non-authorized roles - Case detail: new "Mitarbeiter" tab for assignment management
75 lines
2.1 KiB
Go
75 lines
2.1 KiB
Go
package auth
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// TenantLookup resolves the default tenant for a user.
|
|
// Defined as an interface to avoid circular dependency with services.
|
|
type TenantLookup interface {
|
|
FirstTenantForUser(ctx context.Context, userID uuid.UUID) (*uuid.UUID, error)
|
|
GetUserRole(ctx context.Context, userID, tenantID uuid.UUID) (string, error)
|
|
}
|
|
|
|
// TenantResolver is middleware that resolves the tenant from X-Tenant-ID header
|
|
// or defaults to the user's first tenant.
|
|
type TenantResolver struct {
|
|
lookup TenantLookup
|
|
}
|
|
|
|
func NewTenantResolver(lookup TenantLookup) *TenantResolver {
|
|
return &TenantResolver{lookup: lookup}
|
|
}
|
|
|
|
func (tr *TenantResolver) Resolve(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
userID, ok := UserFromContext(r.Context())
|
|
if !ok {
|
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
var tenantID uuid.UUID
|
|
|
|
if header := r.Header.Get("X-Tenant-ID"); header != "" {
|
|
parsed, err := uuid.Parse(header)
|
|
if err != nil {
|
|
http.Error(w, fmt.Sprintf("invalid X-Tenant-ID: %v", err), http.StatusBadRequest)
|
|
return
|
|
}
|
|
// 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
|
|
}
|
|
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)
|
|
first, err := tr.lookup.FirstTenantForUser(r.Context(), userID)
|
|
if err != nil {
|
|
http.Error(w, fmt.Sprintf("resolving tenant: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
if first == nil {
|
|
http.Error(w, "no tenant found for user", http.StatusBadRequest)
|
|
return
|
|
}
|
|
tenantID = *first
|
|
}
|
|
|
|
ctx := ContextWithTenantID(r.Context(), tenantID)
|
|
next.ServeHTTP(w, r.WithContext(ctx))
|
|
})
|
|
}
|