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
93 lines
2.7 KiB
Go
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
|
|
}
|