#48 — adds 'admin' as fifth project_teams.responsibility value, plumbs an inheritable role-edit gate via the materialised ltree path. - migration 110: ALTER responsibility CHECK, CREATE paliad.effective_project_admin(uuid,uuid) STABLE SECURITY DEFINER (mirrors can_see_project shape), REPLACE project_teams_update / _insert / _delete RLS policies. Idempotent + down-mig provided. Dry-run BEGIN..ROLLBACK clean on live supabase. - services/approval_levels.go: ResponsibilityAdmin const + IsValidResponsibility extension. responsibilityOpensGate UNCHANGED — admin is orthogonal to the 4-Augen approval gate. - services/team_service.go: ChangeResponsibility() with last-admin guard inside tx (counts admins on project + ancestor chain, excludes the row being changed). RemoveMember() also runs the guard when removing an admin row. New IsEffectiveProjectAdmin() driving the frontend affordance. legacyRoleFromResponsibility: admin → 'lead' (deprecated shadow column). - services/project_service.go: ErrLastProjectAdmin sentinel mapped to 409 in writeServiceError. - handlers/teams.go: new PATCH /api/projects/{id}/team/{user_id}. RLS-enforced; non-admins get 404 to avoid existence leakage. - handlers/projects.go: GET /api/projects/{id} now wraps the payload with effective_admin bool so the frontend drives the inline-select affordance without a second round-trip. - frontend/src/projects-detail.tsx + client/projects-detail.ts: admin appears as 5th option in 'Mitglied hinzufügen' dropdown. Team-list Rolle cell switches to an inline <select> for callers with effective_admin (read-only span otherwise). Optimistic PATCH with rollback on error (last-admin guard / 403 from RLS / etc.) surfaced as transient toast in #team-msg. - i18n: +6 keys (admin label + admin.hint + 3 error toasts × 2 langs). - tests: TestIsValidResponsibility now covers admin; new TestLegacyRoleFromResponsibility pins the mapping table. go build && go test -short ./internal/... && bun run build all clean.
66 lines
2.5 KiB
SQL
66 lines
2.5 KiB
SQL
-- Reverse of 111_project_admin_and_select.up.sql.
|
|
--
|
|
-- Drops effective_project_admin, restores the original RLS policies,
|
|
-- and shrinks the responsibility CHECK back to four values. Any rows
|
|
-- still carrying responsibility='admin' would violate the restored
|
|
-- CHECK; the down-migration backfills them to 'lead' (the closest
|
|
-- existing role) before re-adding the constraint.
|
|
|
|
-- ============================================================================
|
|
-- 1. Backfill any responsibility='admin' rows to 'lead'.
|
|
-- ============================================================================
|
|
|
|
UPDATE paliad.project_teams
|
|
SET responsibility = 'lead'
|
|
WHERE responsibility = 'admin';
|
|
|
|
-- ============================================================================
|
|
-- 2. Restore the original CHECK (lead/member/observer/external).
|
|
-- ============================================================================
|
|
|
|
ALTER TABLE paliad.project_teams
|
|
DROP CONSTRAINT IF EXISTS project_teams_responsibility_check;
|
|
|
|
ALTER TABLE paliad.project_teams
|
|
ADD CONSTRAINT project_teams_responsibility_check
|
|
CHECK (responsibility IN ('lead', 'member', 'observer', 'external'));
|
|
|
|
-- ============================================================================
|
|
-- 3. Restore the pre-110 RLS policies.
|
|
-- ============================================================================
|
|
|
|
DROP POLICY IF EXISTS project_teams_update ON paliad.project_teams;
|
|
CREATE POLICY project_teams_update
|
|
ON paliad.project_teams FOR UPDATE
|
|
USING (paliad.can_see_project(project_id))
|
|
WITH CHECK (paliad.can_see_project(project_id));
|
|
|
|
DROP POLICY IF EXISTS project_teams_insert ON paliad.project_teams;
|
|
CREATE POLICY project_teams_insert
|
|
ON paliad.project_teams FOR INSERT
|
|
WITH CHECK (
|
|
user_id = auth.uid()
|
|
OR paliad.can_see_project(project_id)
|
|
);
|
|
|
|
DROP POLICY IF EXISTS project_teams_delete ON paliad.project_teams;
|
|
CREATE POLICY project_teams_delete
|
|
ON paliad.project_teams FOR DELETE
|
|
USING (
|
|
paliad.can_see_project(project_id)
|
|
AND (
|
|
user_id = auth.uid()
|
|
OR EXISTS (
|
|
SELECT 1 FROM paliad.users u
|
|
WHERE u.id = auth.uid()
|
|
AND u.global_role = 'global_admin'
|
|
)
|
|
)
|
|
);
|
|
|
|
-- ============================================================================
|
|
-- 4. Drop the predicate function.
|
|
-- ============================================================================
|
|
|
|
DROP FUNCTION IF EXISTS paliad.effective_project_admin(uuid, uuid);
|