Make contractor residence optional with visibility rules

- Make residence_id nullable in contractor model
- Add created_by_id field to track contractor creator
- Update access control: personal contractors visible only to creator,
  residence contractors visible to all residence users
- Add database migration for schema changes
- Update admin panel DTOs and handlers for optional residence
- Fix test utilities for new model structure

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Trey t
2025-11-29 18:42:11 -06:00
parent 9e91e274e8
commit 4e9b31377b
13 changed files with 123 additions and 97 deletions

View File

@@ -41,12 +41,8 @@ func (s *ContractorService) GetContractor(contractorID, userID uint) (*responses
return nil, err
}
// Check access via residence
hasAccess, err := s.residenceRepo.HasAccess(contractor.ResidenceID, userID)
if err != nil {
return nil, err
}
if !hasAccess {
// Check access
if !s.hasContractorAccess(contractor, userID) {
return nil, ErrContractorAccessDenied
}
@@ -54,6 +50,24 @@ func (s *ContractorService) GetContractor(contractorID, userID uint) (*responses
return &resp, nil
}
// hasContractorAccess checks if user has access to a contractor
// Access rules:
// - If contractor has no residence: only the creator has access
// - If contractor has a residence: all users with access to that residence
func (s *ContractorService) hasContractorAccess(contractor *models.Contractor, userID uint) bool {
if contractor.ResidenceID == nil {
// Personal contractor - only creator has access
return contractor.CreatedByID == userID
}
// Residence contractor - check residence access
hasAccess, err := s.residenceRepo.HasAccess(*contractor.ResidenceID, userID)
if err != nil {
return false
}
return hasAccess
}
// ListContractors lists all contractors accessible to a user
func (s *ContractorService) ListContractors(userID uint) ([]responses.ContractorResponse, error) {
residences, err := s.residenceRepo.FindByUser(userID)
@@ -66,11 +80,8 @@ func (s *ContractorService) ListContractors(userID uint) ([]responses.Contractor
residenceIDs[i] = r.ID
}
if len(residenceIDs) == 0 {
return []responses.ContractorResponse{}, nil
}
contractors, err := s.contractorRepo.FindByUser(residenceIDs)
// FindByUser now handles both personal and residence contractors
contractors, err := s.contractorRepo.FindByUser(userID, residenceIDs)
if err != nil {
return nil, err
}
@@ -80,13 +91,15 @@ func (s *ContractorService) ListContractors(userID uint) ([]responses.Contractor
// CreateContractor creates a new contractor
func (s *ContractorService) CreateContractor(req *requests.CreateContractorRequest, userID uint) (*responses.ContractorResponse, error) {
// Check residence access
hasAccess, err := s.residenceRepo.HasAccess(req.ResidenceID, userID)
if err != nil {
return nil, err
}
if !hasAccess {
return nil, ErrResidenceAccessDenied
// If residence is provided, check access
if req.ResidenceID != nil {
hasAccess, err := s.residenceRepo.HasAccess(*req.ResidenceID, userID)
if err != nil {
return nil, err
}
if !hasAccess {
return nil, ErrResidenceAccessDenied
}
}
isFavorite := false
@@ -124,9 +137,9 @@ func (s *ContractorService) CreateContractor(req *requests.CreateContractorReque
}
// Reload with relations
contractor, err = s.contractorRepo.FindByID(contractor.ID)
if err != nil {
return nil, err
contractor, reloadErr := s.contractorRepo.FindByID(contractor.ID)
if reloadErr != nil {
return nil, reloadErr
}
resp := responses.NewContractorResponse(contractor)
@@ -144,11 +157,7 @@ func (s *ContractorService) UpdateContractor(contractorID, userID uint, req *req
}
// Check access
hasAccess, err := s.residenceRepo.HasAccess(contractor.ResidenceID, userID)
if err != nil {
return nil, err
}
if !hasAccess {
if !s.hasContractorAccess(contractor, userID) {
return nil, ErrContractorAccessDenied
}
@@ -222,11 +231,7 @@ func (s *ContractorService) DeleteContractor(contractorID, userID uint) error {
}
// Check access
hasAccess, err := s.residenceRepo.HasAccess(contractor.ResidenceID, userID)
if err != nil {
return err
}
if !hasAccess {
if !s.hasContractorAccess(contractor, userID) {
return ErrContractorAccessDenied
}
@@ -244,11 +249,7 @@ func (s *ContractorService) ToggleFavorite(contractorID, userID uint) (*response
}
// Check access
hasAccess, err := s.residenceRepo.HasAccess(contractor.ResidenceID, userID)
if err != nil {
return nil, err
}
if !hasAccess {
if !s.hasContractorAccess(contractor, userID) {
return nil, ErrContractorAccessDenied
}
@@ -278,11 +279,7 @@ func (s *ContractorService) GetContractorTasks(contractorID, userID uint) ([]res
}
// Check access
hasAccess, err := s.residenceRepo.HasAccess(contractor.ResidenceID, userID)
if err != nil {
return nil, err
}
if !hasAccess {
if !s.hasContractorAccess(contractor, userID) {
return nil, ErrContractorAccessDenied
}