Adds proposals + proposal_votes + policies + incidents + incident_updates
tables with full RLS, async voting flow, dismissal-appeal pathway,
versioned policies with provenance, and an anonymous-incident token mode.
Adds per-space configurable areas, financial visibility settings, member
self-profile + COI disclosure, and a mission statement that renders in
the app footer.
Reorganises lib/actions.ts into per-domain files (tasks, projects,
members, payments, contacts, knowledge-base, secrets, area-leads,
settings, imports, proposals, incidents, policies, areas) plus a barrel.
Adds lib/permissions.ts and lib/auth-helpers.ts so action bodies stop
duplicating the user-to-member-to-role check. Wires every action that has
a Zod schema; introduces a flexibleDateTime preprocessor so HTML date
pickers no longer hit "Invalid datetime".
Hardens RLS: drops NOT NULL on space_members.user_id for offline members,
adds a privilege-escalation trigger that blocks self-edits to role / tier
/ status / approved / has_card_access / space_id, closes four
cross-tenant holes in the governance kernel, and adds a partial unique
index so two admins cannot concurrently activate two drafts of the same
policy slug.
Ships every deployment artifact: Dockerfile, docker-compose,
.do/app.yaml, .env.example, /api/health, plus a managed-Supabase guide
(docs/DEPLOYMENT.md), an end-to-end self-hosted Droplet guide
(docs/DEPLOY_DO_SELFHOSTED.md), and a Supabase-CLI local-dev quickstart
(docs/LOCAL_DEV.md). Replaces middleware.ts with proxy.ts for Next.js 16.
Adds scripts/schema.sql as the canonical idempotent full deploy plus
fourteen numbered incremental migrations.
Adds scripts/seed.sql so a fresh local DB renders something instead of
empty pages: a demo space, five members across all roles, a v1 code of
conduct, three proposals in different states with seven votes already
cast, an open incident, a project, three tasks, a KB entry, and matching
activity-log rows.
Replaces v0-generated test scaffolding with 266 real tests across
__tests__/{actions,auth-helpers,components,governance,permissions,
security,utils,validations}.test.ts. Adds react-markdown + remark-gfm
for proposal / incident / policy body rendering. Deletes four dead v0
fix-* scripts, the unused components/app-shell.tsx, and the duplicate
app/auth/login/page.tsx.
Routes (31 total): /api/health, /api/paypal/sync, /auth/callback, /,
/login, /signup, /signup/confirm, /dashboard, /tasks, /projects,
/members, /payments, /financials, /contacts, /ops, /ops/[id], /ops/new,
/proposals, /proposals/[id], /proposals/new, /incidents, /incidents/[id],
/incidents/new, /policies, /policies/[slug], /policies/new, /profile,
/recruitment, /settings, /comms, /import.
Full pass-by-pass history is in docs/HANDOFF.md; the roadmap is in
docs/GOVERNANCE_FEATURES.md.
64 lines
2.3 KiB
PL/PgSQL
64 lines
2.3 KiB
PL/PgSQL
-- =============================================================================
|
|
-- 015: Prevent self-role-change on space_members
|
|
-- =============================================================================
|
|
-- Background:
|
|
-- The members_update RLS policy allows a user to UPDATE their own row
|
|
-- (user_id = auth.uid()). This is intentional so members can edit their
|
|
-- display_name, handle, phone, etc. without going through admin.
|
|
-- But the policy has no column-level restriction, so a member could
|
|
-- PATCH their own row via PostgREST and set role = 'admin'.
|
|
--
|
|
-- This migration:
|
|
-- Adds a BEFORE UPDATE trigger that rejects any change to role, tier,
|
|
-- status, approved, has_card_access, or space_id when the row being
|
|
-- updated belongs to the current user AND the current user does not
|
|
-- already hold a privileged role in that space.
|
|
--
|
|
-- Service role (no auth.uid) and admin/board/treasurer self-updates are
|
|
-- unaffected.
|
|
--
|
|
-- Safe to re-run: function is CREATE OR REPLACE; trigger is dropped first.
|
|
-- =============================================================================
|
|
|
|
CREATE OR REPLACE FUNCTION public.prevent_member_self_role_change()
|
|
RETURNS trigger
|
|
LANGUAGE plpgsql SECURITY DEFINER
|
|
SET search_path = public
|
|
AS $$
|
|
DECLARE
|
|
v_is_privileged boolean;
|
|
BEGIN
|
|
IF auth.uid() IS NULL THEN
|
|
RETURN NEW;
|
|
END IF;
|
|
|
|
IF NEW.user_id IS DISTINCT FROM auth.uid() THEN
|
|
RETURN NEW;
|
|
END IF;
|
|
|
|
SELECT public.user_has_role_in_space(auth.uid(), NEW.space_id, ARRAY['admin','board','treasurer'])
|
|
INTO v_is_privileged;
|
|
|
|
IF v_is_privileged THEN
|
|
RETURN NEW;
|
|
END IF;
|
|
|
|
IF NEW.role IS DISTINCT FROM OLD.role
|
|
OR NEW.tier IS DISTINCT FROM OLD.tier
|
|
OR NEW.status IS DISTINCT FROM OLD.status
|
|
OR NEW.approved IS DISTINCT FROM OLD.approved
|
|
OR NEW.has_card_access IS DISTINCT FROM OLD.has_card_access
|
|
OR NEW.space_id IS DISTINCT FROM OLD.space_id THEN
|
|
RAISE EXCEPTION 'Members cannot change their own role, tier, status, approval, card access, or space.'
|
|
USING ERRCODE = '42501';
|
|
END IF;
|
|
|
|
RETURN NEW;
|
|
END;
|
|
$$;
|
|
|
|
DROP TRIGGER IF EXISTS trg_prevent_member_self_role_change ON public.space_members;
|
|
CREATE TRIGGER trg_prevent_member_self_role_change
|
|
BEFORE UPDATE ON public.space_members
|
|
FOR EACH ROW EXECUTE FUNCTION public.prevent_member_self_role_change();
|