Files
KanzlAI-mGMT/backend/internal/services/case_assignment_service.go
m 0a0ec016d8 feat: role-based permissions (owner/partner/associate/paralegal/secretary)
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
2026-03-30 11:04:57 +02:00

93 lines
2.7 KiB
Go

package services
import (
"context"
"fmt"
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
"mgit.msbls.de/m/KanzlAI-mGMT/internal/models"
)
type CaseAssignmentService struct {
db *sqlx.DB
}
func NewCaseAssignmentService(db *sqlx.DB) *CaseAssignmentService {
return &CaseAssignmentService{db: db}
}
// ListByCase returns all assignments for a case.
func (s *CaseAssignmentService) ListByCase(ctx context.Context, tenantID, caseID uuid.UUID) ([]models.CaseAssignment, error) {
var assignments []models.CaseAssignment
err := s.db.SelectContext(ctx, &assignments,
`SELECT ca.id, ca.case_id, ca.user_id, ca.role, ca.assigned_at
FROM case_assignments ca
JOIN cases c ON c.id = ca.case_id
WHERE ca.case_id = $1 AND c.tenant_id = $2
ORDER BY ca.assigned_at`,
caseID, tenantID)
if err != nil {
return nil, fmt.Errorf("list case assignments: %w", err)
}
return assignments, nil
}
// Assign adds a user to a case with the given role.
func (s *CaseAssignmentService) Assign(ctx context.Context, tenantID, caseID, userID uuid.UUID, role string) (*models.CaseAssignment, error) {
// Verify user is a member of this tenant
var memberExists bool
err := s.db.GetContext(ctx, &memberExists,
`SELECT EXISTS(SELECT 1 FROM user_tenants WHERE user_id = $1 AND tenant_id = $2)`,
userID, tenantID)
if err != nil {
return nil, fmt.Errorf("check membership: %w", err)
}
if !memberExists {
return nil, fmt.Errorf("user is not a member of this tenant")
}
// Verify case belongs to tenant
var caseExists bool
err = s.db.GetContext(ctx, &caseExists,
`SELECT EXISTS(SELECT 1 FROM cases WHERE id = $1 AND tenant_id = $2)`,
caseID, tenantID)
if err != nil {
return nil, fmt.Errorf("check case: %w", err)
}
if !caseExists {
return nil, fmt.Errorf("case not found")
}
var assignment models.CaseAssignment
err = s.db.QueryRowxContext(ctx,
`INSERT INTO case_assignments (case_id, user_id, role)
VALUES ($1, $2, $3)
ON CONFLICT (case_id, user_id) DO UPDATE SET role = EXCLUDED.role
RETURNING id, case_id, user_id, role, assigned_at`,
caseID, userID, role,
).StructScan(&assignment)
if err != nil {
return nil, fmt.Errorf("assign user to case: %w", err)
}
return &assignment, nil
}
// Unassign removes a user from a case.
func (s *CaseAssignmentService) Unassign(ctx context.Context, tenantID, caseID, userID uuid.UUID) error {
result, err := s.db.ExecContext(ctx,
`DELETE FROM case_assignments ca
USING cases c
WHERE ca.case_id = c.id AND ca.case_id = $1 AND ca.user_id = $2 AND c.tenant_id = $3`,
caseID, userID, tenantID)
if err != nil {
return fmt.Errorf("unassign: %w", err)
}
rows, _ := result.RowsAffected()
if rows == 0 {
return fmt.Errorf("assignment not found")
}
return nil
}