feat: role-based permissions — owner/partner/associate/paralegal/secretary (P0)

This commit is contained in:
m
2026-03-30 11:09:05 +02:00
20 changed files with 1112 additions and 123 deletions

View File

@@ -12,7 +12,12 @@ 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
@@ -41,6 +46,7 @@ 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)
@@ -54,9 +60,24 @@ func (tr *TenantResolver) Resolve(next http.Handler) http.Handler {
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
// Default to user's first tenant (role already set by middleware)
first, err := tr.lookup.FirstTenantForUser(r.Context(), userID)
if err != nil {
slog.Error("failed to resolve default tenant", "error", err, "user_id", userID)