package auth import ( "context" "encoding/json" "errors" "github.com/google/uuid" "github.com/jackc/pgx/v5" ) // ErrMemberNotFound is returned when the target user has no membership in the company. var ErrMemberNotFound = errors.New("member not found") // MemberPermissions returns the stored per-member access overlay (a sparse map of // dashboard feature key -> false). Empty means unrestricted. // // This is the raw stored value for the owner-facing editor; capability resolution for // a live request goes through billing.CapabilitiesForMember, which also ignores the // overlay for the company owner and intersects it with the plan. func (s *Service) MemberPermissions(ctx context.Context, companyID, userID uuid.UUID) (map[string]bool, error) { var raw []byte err := s.Pool.QueryRow(ctx, ` SELECT COALESCE(permissions, '{}'::jsonb) FROM memberships WHERE company_id = $1 AND user_id = $2`, companyID, userID).Scan(&raw) if errors.Is(err, pgx.ErrNoRows) { return nil, ErrMemberNotFound } if err != nil { return nil, err } out := map[string]bool{} if len(raw) > 0 { if err := json.Unmarshal(raw, &out); err != nil { return map[string]bool{}, nil } } return out, nil } // SetMemberPermissions replaces the overlay for one member. Callers must sanitize the // map first (billing.SanitizeMemberPermissions) — this layer only persists it. func (s *Service) SetMemberPermissions(ctx context.Context, companyID, userID uuid.UUID, perms map[string]bool) error { if perms == nil { perms = map[string]bool{} } encoded, err := json.Marshal(perms) if err != nil { return err } tag, err := s.Pool.Exec(ctx, ` UPDATE memberships SET permissions = $3::jsonb, updated_at = now() WHERE company_id = $1 AND user_id = $2`, companyID, userID, encoded) if err != nil { return err } if tag.RowsAffected() == 0 { return ErrMemberNotFound } return nil } // MemberPermissionsByCompany returns every restricted member's overlay for one company, // keyed by user id. Members with an empty overlay are omitted, so the team list can show // a "restricted" badge without an N+1 query. func (s *Service) MemberPermissionsByCompany(ctx context.Context, companyID uuid.UUID) (map[uuid.UUID]map[string]bool, error) { rows, err := s.Pool.Query(ctx, ` SELECT user_id, permissions FROM memberships WHERE company_id = $1 AND permissions <> '{}'::jsonb`, companyID) if err != nil { return nil, err } defer rows.Close() out := map[uuid.UUID]map[string]bool{} for rows.Next() { var userID uuid.UUID var raw []byte if err := rows.Scan(&userID, &raw); err != nil { return nil, err } perms := map[string]bool{} if len(raw) > 0 { if err := json.Unmarshal(raw, &perms); err != nil { continue } } if len(perms) > 0 { out[userID] = perms } } return out, rows.Err() }