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
214 lines
6.0 KiB
Go
214 lines
6.0 KiB
Go
package auth
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jmoiron/sqlx"
|
|
)
|
|
|
|
// Valid roles ordered by privilege level (highest first).
|
|
var ValidRoles = []string{"owner", "partner", "associate", "paralegal", "secretary"}
|
|
|
|
// IsValidRole checks if a role string is one of the defined roles.
|
|
func IsValidRole(role string) bool {
|
|
for _, r := range ValidRoles {
|
|
if r == role {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Permission represents an action that can be checked against roles.
|
|
type Permission int
|
|
|
|
const (
|
|
PermManageTeam Permission = iota
|
|
PermManageBilling
|
|
PermCreateCase
|
|
PermEditAllCases
|
|
PermEditAssignedCase
|
|
PermViewAllCases
|
|
PermManageDeadlines
|
|
PermManageAppointments
|
|
PermUploadDocuments
|
|
PermDeleteDocuments
|
|
PermDeleteOwnDocuments
|
|
PermViewAuditLog
|
|
PermManageSettings
|
|
PermAIExtraction
|
|
)
|
|
|
|
// rolePermissions maps each role to its set of permissions.
|
|
var rolePermissions = map[string]map[Permission]bool{
|
|
"owner": {
|
|
PermManageTeam: true,
|
|
PermManageBilling: true,
|
|
PermCreateCase: true,
|
|
PermEditAllCases: true,
|
|
PermEditAssignedCase: true,
|
|
PermViewAllCases: true,
|
|
PermManageDeadlines: true,
|
|
PermManageAppointments: true,
|
|
PermUploadDocuments: true,
|
|
PermDeleteDocuments: true,
|
|
PermDeleteOwnDocuments: true,
|
|
PermViewAuditLog: true,
|
|
PermManageSettings: true,
|
|
PermAIExtraction: true,
|
|
},
|
|
"partner": {
|
|
PermManageTeam: true,
|
|
PermManageBilling: true,
|
|
PermCreateCase: true,
|
|
PermEditAllCases: true,
|
|
PermEditAssignedCase: true,
|
|
PermViewAllCases: true,
|
|
PermManageDeadlines: true,
|
|
PermManageAppointments: true,
|
|
PermUploadDocuments: true,
|
|
PermDeleteDocuments: true,
|
|
PermDeleteOwnDocuments: true,
|
|
PermViewAuditLog: true,
|
|
PermManageSettings: true,
|
|
PermAIExtraction: true,
|
|
},
|
|
"associate": {
|
|
PermCreateCase: true,
|
|
PermEditAssignedCase: true,
|
|
PermViewAllCases: true,
|
|
PermManageDeadlines: true,
|
|
PermManageAppointments: true,
|
|
PermUploadDocuments: true,
|
|
PermDeleteOwnDocuments: true,
|
|
PermAIExtraction: true,
|
|
},
|
|
"paralegal": {
|
|
PermEditAssignedCase: true,
|
|
PermViewAllCases: true,
|
|
PermManageDeadlines: true,
|
|
PermManageAppointments: true,
|
|
PermUploadDocuments: true,
|
|
},
|
|
"secretary": {
|
|
PermViewAllCases: true,
|
|
PermManageAppointments: true,
|
|
PermUploadDocuments: true,
|
|
},
|
|
}
|
|
|
|
// HasPermission checks if the given role has the specified permission.
|
|
func HasPermission(role string, perm Permission) bool {
|
|
perms, ok := rolePermissions[role]
|
|
if !ok {
|
|
return false
|
|
}
|
|
return perms[perm]
|
|
}
|
|
|
|
// RequirePermission returns middleware that checks if the user's role has the given permission.
|
|
func RequirePermission(perm Permission) func(http.Handler) http.Handler {
|
|
return func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
role := UserRoleFromContext(r.Context())
|
|
if role == "" || !HasPermission(role, perm) {
|
|
writeJSONError(w, "insufficient permissions", http.StatusForbidden)
|
|
return
|
|
}
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
}
|
|
|
|
// RequireRole returns middleware that checks if the user has one of the specified roles.
|
|
func RequireRole(roles ...string) func(http.Handler) http.Handler {
|
|
allowed := make(map[string]bool, len(roles))
|
|
for _, r := range roles {
|
|
allowed[r] = true
|
|
}
|
|
return func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
role := UserRoleFromContext(r.Context())
|
|
if !allowed[role] {
|
|
writeJSONError(w, "insufficient permissions", http.StatusForbidden)
|
|
return
|
|
}
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
}
|
|
|
|
// IsAssignedToCase checks if a user is assigned to a specific case.
|
|
func IsAssignedToCase(ctx context.Context, db *sqlx.DB, userID, caseID uuid.UUID) (bool, error) {
|
|
var exists bool
|
|
err := db.GetContext(ctx, &exists,
|
|
`SELECT EXISTS(SELECT 1 FROM case_assignments WHERE user_id = $1 AND case_id = $2)`,
|
|
userID, caseID)
|
|
return exists, err
|
|
}
|
|
|
|
// CanEditCase checks if a user can edit a specific case based on role and assignment.
|
|
func CanEditCase(ctx context.Context, db *sqlx.DB, userID, caseID uuid.UUID, role string) (bool, error) {
|
|
// Owner and partner can edit all cases
|
|
if HasPermission(role, PermEditAllCases) {
|
|
return true, nil
|
|
}
|
|
// Others need to be assigned
|
|
if !HasPermission(role, PermEditAssignedCase) {
|
|
return false, nil
|
|
}
|
|
return IsAssignedToCase(ctx, db, userID, caseID)
|
|
}
|
|
|
|
// CanDeleteDocument checks if a user can delete a specific document.
|
|
func CanDeleteDocument(role string, docUploaderID, userID uuid.UUID) bool {
|
|
if HasPermission(role, PermDeleteDocuments) {
|
|
return true
|
|
}
|
|
if HasPermission(role, PermDeleteOwnDocuments) {
|
|
return docUploaderID == userID
|
|
}
|
|
return false
|
|
}
|
|
|
|
// permissionNames maps Permission constants to their string names for frontend use.
|
|
var permissionNames = map[Permission]string{
|
|
PermManageTeam: "manage_team",
|
|
PermManageBilling: "manage_billing",
|
|
PermCreateCase: "create_case",
|
|
PermEditAllCases: "edit_all_cases",
|
|
PermEditAssignedCase: "edit_assigned_case",
|
|
PermViewAllCases: "view_all_cases",
|
|
PermManageDeadlines: "manage_deadlines",
|
|
PermManageAppointments: "manage_appointments",
|
|
PermUploadDocuments: "upload_documents",
|
|
PermDeleteDocuments: "delete_documents",
|
|
PermDeleteOwnDocuments: "delete_own_documents",
|
|
PermViewAuditLog: "view_audit_log",
|
|
PermManageSettings: "manage_settings",
|
|
PermAIExtraction: "ai_extraction",
|
|
}
|
|
|
|
// GetRolePermissions returns a list of permission name strings for the given role.
|
|
func GetRolePermissions(role string) []string {
|
|
perms, ok := rolePermissions[role]
|
|
if !ok {
|
|
return nil
|
|
}
|
|
var names []string
|
|
for p := range perms {
|
|
if name, ok := permissionNames[p]; ok {
|
|
names = append(names, name)
|
|
}
|
|
}
|
|
return names
|
|
}
|
|
|
|
func writeJSONError(w http.ResponseWriter, msg string, status int) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(status)
|
|
w.Write([]byte(`{"error":"` + msg + `"}`))
|
|
}
|