fix: resolve session refresh issue post space creation
Ensure session cookies are updated before server navigation for both new and returning users. Co-authored-by: Moheeb Zara <[email protected]>
This commit is contained in:
+347
-351
@@ -1,49 +1,50 @@
|
||||
-- =============================================================================
|
||||
-- HACKERSPACE.SH — CANONICAL SCHEMA (single source of truth)
|
||||
-- HACKERSPACE MANAGEMENT — CANONICAL SCHEMA
|
||||
-- =============================================================================
|
||||
-- Run this file ONCE on a brand-new Supabase project.
|
||||
-- It is fully idempotent: safe to run again if interrupted.
|
||||
-- Single file. Run top-to-bottom in Supabase SQL Editor on a fresh project.
|
||||
-- Idempotent: uses IF NOT EXISTS / OR REPLACE throughout.
|
||||
-- No supplemental scripts required.
|
||||
--
|
||||
-- Execution order:
|
||||
-- Sections:
|
||||
-- 1. Extensions
|
||||
-- 2. Enums
|
||||
-- 3. Tables (spaces → space_members → everything else)
|
||||
-- 4. Unique constraints & indexes
|
||||
-- 5. Helper functions (reference space_members → must come after tables)
|
||||
-- 3. Tables (spaces → space_members → rest)
|
||||
-- 4. Indexes
|
||||
-- 5. Helper functions (must come after tables)
|
||||
-- 6. Row Level Security
|
||||
-- 7. Triggers & auth hook
|
||||
-- 8. Realtime
|
||||
-- =============================================================================
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 1. EXTENSIONS
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 2. ENUMS
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
DO $$ BEGIN CREATE TYPE public.member_role AS ENUM ('admin','board','treasurer','member','associate'); EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||
DO $$ BEGIN CREATE TYPE public.member_tier AS ENUM ('plus','basic','associate'); EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||
DO $$ BEGIN CREATE TYPE public.member_status AS ENUM ('current','late','inactive','unverified'); EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||
DO $$ BEGIN CREATE TYPE public.task_status AS ENUM ('open','claimed','in_progress','overdue','due_today','completed','done','blocked'); EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||
DO $$ BEGIN CREATE TYPE public.task_type AS ENUM ('chore','task'); EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||
DO $$ BEGIN CREATE TYPE public.recurrence_type AS ENUM ('daily','weekly','biweekly','monthly','none'); EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||
DO $$ BEGIN CREATE TYPE public.project_status AS ENUM ('backlog','in_progress','review','done','blocked'); EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||
DO $$ BEGIN CREATE TYPE public.payment_platform AS ENUM ('paypal','zeffy','venmo','cash'); EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||
DO $$ BEGIN CREATE TYPE public.payment_link_status AS ENUM ('linked','unlinked'); EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||
DO $$ BEGIN CREATE TYPE public.kb_visibility AS ENUM ('all_members','board','admin_only'); EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||
DO $$ BEGIN CREATE TYPE public.area_lead_status AS ENUM ('active','vacant','handoff'); EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||
DO $$ BEGIN CREATE TYPE public.contact_type AS ENUM ('vendor','supplier','partner','landlord','city'); EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||
DO $$ BEGIN CREATE TYPE public.channel_type AS ENUM ('general','area','ops','project'); EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||
-- -----------------------------------------------------------------------------
|
||||
DO $$ BEGIN CREATE TYPE public.member_role AS ENUM ('admin','board','treasurer','member','associate'); EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||
DO $$ BEGIN CREATE TYPE public.member_tier AS ENUM ('plus','basic','associate'); EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||
DO $$ BEGIN CREATE TYPE public.member_status AS ENUM ('current','late','inactive','unverified'); EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||
DO $$ BEGIN CREATE TYPE public.task_status AS ENUM ('open','claimed','in_progress','overdue','due_today','completed','done','blocked'); EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||
DO $$ BEGIN CREATE TYPE public.task_type AS ENUM ('chore','task'); EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||
DO $$ BEGIN CREATE TYPE public.recurrence_type AS ENUM ('daily','weekly','biweekly','monthly','none'); EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||
DO $$ BEGIN CREATE TYPE public.project_status AS ENUM ('backlog','in_progress','review','done','blocked'); EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||
DO $$ BEGIN CREATE TYPE public.payment_platform AS ENUM ('paypal','zeffy','venmo','cash'); EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||
DO $$ BEGIN CREATE TYPE public.payment_link_status AS ENUM ('linked','unlinked'); EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||
DO $$ BEGIN CREATE TYPE public.kb_visibility AS ENUM ('all_members','board','admin_only'); EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||
DO $$ BEGIN CREATE TYPE public.area_lead_status AS ENUM ('active','vacant','handoff'); EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||
DO $$ BEGIN CREATE TYPE public.contact_type AS ENUM ('vendor','supplier','partner','landlord','city'); EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||
DO $$ BEGIN CREATE TYPE public.channel_type AS ENUM ('general','area','ops','project'); EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- 3. TABLES (dependency order)
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 3. TABLES
|
||||
-- -----------------------------------------------------------------------------
|
||||
|
||||
-- ── spaces ───────────────────────────────────────────────────────────────────
|
||||
-- spaces ----------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS public.spaces (
|
||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
name text NOT NULL,
|
||||
@@ -52,24 +53,21 @@ CREATE TABLE IF NOT EXISTS public.spaces (
|
||||
logo_url text,
|
||||
city text,
|
||||
address text,
|
||||
timezone text NOT NULL DEFAULT 'America/Phoenix',
|
||||
timezone text DEFAULT 'America/Phoenix',
|
||||
invite_code text,
|
||||
require_approval boolean NOT NULL DEFAULT true,
|
||||
public_member_directory boolean NOT NULL DEFAULT false,
|
||||
require_approval boolean DEFAULT true,
|
||||
public_member_directory boolean DEFAULT false,
|
||||
webhook_secret text,
|
||||
settings jsonb NOT NULL DEFAULT '{}',
|
||||
settings jsonb DEFAULT '{}',
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- ── space_members ─────────────────────────────────────────────────────────────
|
||||
-- One row per (user × space).
|
||||
-- display_name — the member's chosen name within this space
|
||||
-- handle — optional @handle (unique within a space is enforced by app, not DB)
|
||||
-- space_members ---------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS public.space_members (
|
||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
space_id uuid NOT NULL REFERENCES public.spaces(id) ON DELETE CASCADE,
|
||||
user_id uuid REFERENCES auth.users(id) ON DELETE SET NULL,
|
||||
user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||
role public.member_role NOT NULL DEFAULT 'member',
|
||||
tier public.member_tier NOT NULL DEFAULT 'basic',
|
||||
status public.member_status NOT NULL DEFAULT 'unverified',
|
||||
@@ -83,8 +81,9 @@ CREATE TABLE IF NOT EXISTS public.space_members (
|
||||
has_card_access boolean NOT NULL DEFAULT false,
|
||||
payment_status text,
|
||||
payment_note text,
|
||||
last_payment_at timestamptz,
|
||||
last_paid_at timestamptz,
|
||||
joined_at timestamptz NOT NULL DEFAULT now(),
|
||||
joined_at timestamptz DEFAULT now(),
|
||||
dues_paid_until timestamptz,
|
||||
stripe_customer_id text,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
@@ -92,44 +91,55 @@ CREATE TABLE IF NOT EXISTS public.space_members (
|
||||
UNIQUE (space_id, user_id)
|
||||
);
|
||||
|
||||
-- ── projects ─────────────────────────────────────────────────────────────────
|
||||
-- Must be created before tasks (tasks.project_id FK).
|
||||
-- Partial unique index: enforce uniqueness on email only when present
|
||||
-- Required by importMembers upsert (onConflict: 'space_id,email')
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_space_members_space_email
|
||||
ON public.space_members (space_id, email)
|
||||
WHERE email IS NOT NULL;
|
||||
|
||||
-- projects --------------------------------------------------------------------
|
||||
-- Created before tasks because tasks.project_id references it.
|
||||
CREATE TABLE IF NOT EXISTS public.projects (
|
||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
space_id uuid NOT NULL REFERENCES public.spaces(id) ON DELETE CASCADE,
|
||||
title text NOT NULL,
|
||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
space_id uuid NOT NULL REFERENCES public.spaces(id) ON DELETE CASCADE,
|
||||
title text NOT NULL,
|
||||
name text,
|
||||
description text,
|
||||
status public.project_status NOT NULL DEFAULT 'backlog',
|
||||
area text,
|
||||
tags text[],
|
||||
category text,
|
||||
status public.project_status NOT NULL DEFAULT 'backlog',
|
||||
lead_id uuid REFERENCES auth.users(id) ON DELETE SET NULL,
|
||||
assignees text,
|
||||
assignee_names text[],
|
||||
due_date timestamptz,
|
||||
task_count integer NOT NULL DEFAULT 0,
|
||||
tasks_completed integer NOT NULL DEFAULT 0,
|
||||
progress integer NOT NULL DEFAULT 0,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
tags text[],
|
||||
task_count integer NOT NULL DEFAULT 0,
|
||||
tasks_completed integer NOT NULL DEFAULT 0,
|
||||
progress integer NOT NULL DEFAULT 0,
|
||||
created_by uuid REFERENCES auth.users(id) ON DELETE SET NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- ── tasks ────────────────────────────────────────────────────────────────────
|
||||
-- task_type — the enum column (canonical)
|
||||
-- status — task lifecycle; both 'completed' and 'done' mean finished
|
||||
-- tasks -----------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS public.tasks (
|
||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
space_id uuid NOT NULL REFERENCES public.spaces(id) ON DELETE CASCADE,
|
||||
space_id uuid NOT NULL REFERENCES public.spaces(id) ON DELETE CASCADE,
|
||||
title text NOT NULL,
|
||||
description text,
|
||||
task_type public.task_type NOT NULL DEFAULT 'task',
|
||||
status public.task_status NOT NULL DEFAULT 'open',
|
||||
recurrence public.recurrence_type NOT NULL DEFAULT 'none',
|
||||
priority text NOT NULL DEFAULT 'medium',
|
||||
recurrence public.recurrence_type DEFAULT 'none',
|
||||
priority text DEFAULT 'medium',
|
||||
area text,
|
||||
project_id uuid REFERENCES public.projects(id) ON DELETE SET NULL,
|
||||
tags text[],
|
||||
due_date timestamptz,
|
||||
project_id uuid REFERENCES public.projects(id) ON DELETE SET NULL,
|
||||
claimed_by uuid REFERENCES auth.users(id) ON DELETE SET NULL,
|
||||
claimed_by_name text,
|
||||
assigned_to uuid REFERENCES auth.users(id) ON DELETE SET NULL,
|
||||
assigned_to_name text,
|
||||
created_by uuid REFERENCES auth.users(id) ON DELETE SET NULL,
|
||||
requested_by uuid REFERENCES auth.users(id) ON DELETE SET NULL,
|
||||
requested_by_name text,
|
||||
subtask_total integer NOT NULL DEFAULT 0,
|
||||
@@ -137,144 +147,167 @@ CREATE TABLE IF NOT EXISTS public.tasks (
|
||||
progress integer NOT NULL DEFAULT 0,
|
||||
last_done_at timestamptz,
|
||||
completed_at timestamptz,
|
||||
created_by uuid REFERENCES auth.users(id) ON DELETE SET NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- ── payments ─────────────────────────────────────────────────────────────────
|
||||
-- from_identifier — email / @handle / name from source platform
|
||||
-- from_note — memo / note from sender
|
||||
-- link_status — whether this payment has been matched to a member
|
||||
-- payments --------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS public.payments (
|
||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
space_id uuid NOT NULL REFERENCES public.spaces(id) ON DELETE CASCADE,
|
||||
space_id uuid NOT NULL REFERENCES public.spaces(id) ON DELETE CASCADE,
|
||||
member_id uuid REFERENCES public.space_members(id) ON DELETE SET NULL,
|
||||
platform public.payment_platform NOT NULL,
|
||||
amount numeric(10,2) NOT NULL,
|
||||
currency text NOT NULL DEFAULT 'USD',
|
||||
description text,
|
||||
-- status kept for backwards compatibility; link_status is canonical
|
||||
status public.payment_link_status NOT NULL DEFAULT 'unlinked',
|
||||
link_status public.payment_link_status NOT NULL DEFAULT 'unlinked',
|
||||
external_id text,
|
||||
from_identifier text,
|
||||
from_note text,
|
||||
member_id uuid REFERENCES public.space_members(id) ON DELETE SET NULL,
|
||||
member_name text,
|
||||
link_status public.payment_link_status NOT NULL DEFAULT 'unlinked',
|
||||
transaction_date timestamptz NOT NULL DEFAULT now(),
|
||||
created_at timestamptz NOT NULL DEFAULT now()
|
||||
payer_name text,
|
||||
payer_email text,
|
||||
payment_date timestamptz,
|
||||
transaction_date timestamptz DEFAULT now(),
|
||||
raw_data jsonb DEFAULT '{}',
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- ── contacts ─────────────────────────────────────────────────────────────────
|
||||
-- code — short auto-generated identifier (e.g. "VEN234")
|
||||
-- contacts --------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS public.contacts (
|
||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
space_id uuid NOT NULL REFERENCES public.spaces(id) ON DELETE CASCADE,
|
||||
name text NOT NULL,
|
||||
code text NOT NULL,
|
||||
contact_type public.contact_type NOT NULL DEFAULT 'vendor',
|
||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
space_id uuid NOT NULL REFERENCES public.spaces(id) ON DELETE CASCADE,
|
||||
name text NOT NULL,
|
||||
contact_type public.contact_type NOT NULL DEFAULT 'vendor',
|
||||
code text,
|
||||
email text,
|
||||
phone text,
|
||||
address text,
|
||||
notes text,
|
||||
details text,
|
||||
note text,
|
||||
group_label text,
|
||||
tags text[],
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
created_by uuid REFERENCES auth.users(id) ON DELETE SET NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- ── knowledge_base ────────────────────────────────────────────────────────────
|
||||
-- is_pinned — canonical pinned flag
|
||||
-- updated_by_id — FK to space_members (for display in UI)
|
||||
-- updated_by_name — denormalized display name for performance
|
||||
-- knowledge_base --------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS public.knowledge_base (
|
||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
space_id uuid NOT NULL REFERENCES public.spaces(id) ON DELETE CASCADE,
|
||||
title text NOT NULL,
|
||||
description text,
|
||||
content text,
|
||||
icon text,
|
||||
visibility public.kb_visibility NOT NULL DEFAULT 'all_members',
|
||||
category text,
|
||||
area text,
|
||||
icon text,
|
||||
tags text[],
|
||||
visibility public.kb_visibility NOT NULL DEFAULT 'all_members',
|
||||
access_level text DEFAULT 'all_members',
|
||||
-- pinned = legacy column, is_pinned = canonical
|
||||
pinned boolean NOT NULL DEFAULT false,
|
||||
is_pinned boolean NOT NULL DEFAULT false,
|
||||
created_by uuid REFERENCES auth.users(id) ON DELETE SET NULL,
|
||||
-- updated_by = auth.users FK (legacy), updated_by_id = space_members FK (canonical)
|
||||
updated_by uuid REFERENCES auth.users(id) ON DELETE SET NULL,
|
||||
updated_by_id uuid REFERENCES public.space_members(id) ON DELETE SET NULL,
|
||||
updated_by_name text,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- ── secrets ──────────────────────────────────────────────────────────────────
|
||||
-- Shared credentials vault. Admin write, board+ read.
|
||||
-- value stored as plain text — encryption is a future enhancement.
|
||||
-- secrets ---------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS public.secrets (
|
||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
space_id uuid NOT NULL REFERENCES public.spaces(id) ON DELETE CASCADE,
|
||||
title text NOT NULL,
|
||||
icon text,
|
||||
description text,
|
||||
-- label = legacy column, title = canonical
|
||||
label text NOT NULL,
|
||||
title text,
|
||||
value text NOT NULL,
|
||||
description text,
|
||||
category text DEFAULT 'general',
|
||||
area text,
|
||||
icon text,
|
||||
notes text,
|
||||
created_by uuid REFERENCES auth.users(id) ON DELETE SET NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- ── area_leads ───────────────────────────────────────────────────────────────
|
||||
-- area_code must be unique within a space (used as upsert conflict target).
|
||||
-- area_leads ------------------------------------------------------------------
|
||||
-- area_code is the upsert conflict target (unique within a space)
|
||||
CREATE TABLE IF NOT EXISTS public.area_leads (
|
||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
space_id uuid NOT NULL REFERENCES public.spaces(id) ON DELETE CASCADE,
|
||||
area_name text NOT NULL,
|
||||
area_code text NOT NULL,
|
||||
lead_id uuid REFERENCES public.space_members(id) ON DELETE SET NULL,
|
||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
space_id uuid NOT NULL REFERENCES public.spaces(id) ON DELETE CASCADE,
|
||||
area_name text NOT NULL,
|
||||
area_code text,
|
||||
description text,
|
||||
notes text,
|
||||
lead_id uuid REFERENCES public.space_members(id) ON DELETE SET NULL,
|
||||
lead_handle text,
|
||||
status public.area_lead_status NOT NULL DEFAULT 'active',
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
UNIQUE (space_id, area_code)
|
||||
status public.area_lead_status NOT NULL DEFAULT 'active',
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT area_leads_space_area_code_key UNIQUE (space_id, area_code) DEFERRABLE INITIALLY DEFERRED
|
||||
);
|
||||
|
||||
-- ── integrations ─────────────────────────────────────────────────────────────
|
||||
-- config JSONB holds platform-specific credentials (e.g. client_id, token).
|
||||
-- platform must be unique within a space (used as upsert conflict target).
|
||||
-- integrations ----------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS public.integrations (
|
||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
space_id uuid NOT NULL REFERENCES public.spaces(id) ON DELETE CASCADE,
|
||||
name text NOT NULL,
|
||||
platform text NOT NULL,
|
||||
name text,
|
||||
description text,
|
||||
is_connected boolean NOT NULL DEFAULT false,
|
||||
-- credentials/settings kept for legacy; config is canonical
|
||||
credentials jsonb DEFAULT '{}',
|
||||
settings jsonb DEFAULT '{}',
|
||||
config jsonb,
|
||||
last_sync_at timestamptz,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
UNIQUE (space_id, platform)
|
||||
);
|
||||
|
||||
-- ── comms_channels ───────────────────────────────────────────────────────────
|
||||
-- name must be unique within a space.
|
||||
-- comms_channels --------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS public.comms_channels (
|
||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
space_id uuid NOT NULL REFERENCES public.spaces(id) ON DELETE CASCADE,
|
||||
name text NOT NULL,
|
||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
space_id uuid NOT NULL REFERENCES public.spaces(id) ON DELETE CASCADE,
|
||||
name text NOT NULL,
|
||||
description text,
|
||||
channel_type public.channel_type NOT NULL DEFAULT 'general',
|
||||
icon text,
|
||||
channel_type public.channel_type NOT NULL DEFAULT 'general',
|
||||
area_reference text,
|
||||
project_id uuid REFERENCES public.projects(id) ON DELETE SET NULL,
|
||||
member_count integer NOT NULL DEFAULT 0,
|
||||
unread_count integer NOT NULL DEFAULT 0,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
UNIQUE (space_id, name)
|
||||
is_default boolean NOT NULL DEFAULT false,
|
||||
is_archived boolean NOT NULL DEFAULT false,
|
||||
member_count integer NOT NULL DEFAULT 0,
|
||||
unread_count integer NOT NULL DEFAULT 0,
|
||||
created_by uuid REFERENCES auth.users(id) ON DELETE SET NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- ── comms_messages ───────────────────────────────────────────────────────────
|
||||
-- comms_messages --------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS public.comms_messages (
|
||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
channel_id uuid NOT NULL REFERENCES public.comms_channels(id) ON DELETE CASCADE,
|
||||
space_id uuid NOT NULL REFERENCES public.spaces(id) ON DELETE CASCADE,
|
||||
user_id uuid REFERENCES auth.users(id) ON DELETE SET NULL,
|
||||
display_name text NOT NULL,
|
||||
space_id uuid NOT NULL REFERENCES public.spaces(id) ON DELETE CASCADE,
|
||||
user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||
display_name text,
|
||||
handle text,
|
||||
content text NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now()
|
||||
edited boolean NOT NULL DEFAULT false,
|
||||
deleted boolean NOT NULL DEFAULT false,
|
||||
reply_to uuid REFERENCES public.comms_messages(id) ON DELETE SET NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- ── activity_log ─────────────────────────────────────────────────────────────
|
||||
-- Append-only audit log. No UPDATE/DELETE policies.
|
||||
-- activity_log ----------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS public.activity_log (
|
||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
space_id uuid NOT NULL REFERENCES public.spaces(id) ON DELETE CASCADE,
|
||||
@@ -284,50 +317,48 @@ CREATE TABLE IF NOT EXISTS public.activity_log (
|
||||
entity_type text,
|
||||
entity_id uuid,
|
||||
details text,
|
||||
metadata jsonb DEFAULT '{}',
|
||||
created_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- 4. UNIQUE CONSTRAINTS & INDEXES
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
-- Partial unique index: (space_id, email) only where email IS NOT NULL
|
||||
-- Required by importMembers upsert in actions.ts
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_space_members_space_email
|
||||
ON public.space_members (space_id, email)
|
||||
WHERE email IS NOT NULL;
|
||||
|
||||
-- Performance indexes
|
||||
CREATE INDEX IF NOT EXISTS idx_space_members_user_id ON public.space_members (user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_space_members_space_id ON public.space_members (space_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_tasks_space_id ON public.tasks (space_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_tasks_status ON public.tasks (status);
|
||||
CREATE INDEX IF NOT EXISTS idx_tasks_claimed_by ON public.tasks (claimed_by);
|
||||
CREATE INDEX IF NOT EXISTS idx_tasks_assigned_to ON public.tasks (assigned_to);
|
||||
CREATE INDEX IF NOT EXISTS idx_projects_space_id ON public.projects (space_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_payments_space_id ON public.payments (space_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_payments_link_status ON public.payments (link_status);
|
||||
CREATE INDEX IF NOT EXISTS idx_payments_transaction_date ON public.payments (transaction_date DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_kb_space_id ON public.knowledge_base (space_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_secrets_space_id ON public.secrets (space_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_contacts_space_id ON public.contacts (space_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_area_leads_space_id ON public.area_leads (space_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_integrations_space_id ON public.integrations (space_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_comms_messages_channel ON public.comms_messages (channel_id, created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_comms_messages_space ON public.comms_messages (space_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_activity_log_space_id ON public.activity_log (space_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_activity_log_created_at ON public.activity_log (created_at DESC);
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 4. INDEXES
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE INDEX IF NOT EXISTS idx_space_members_user_id ON public.space_members (user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_space_members_space_id ON public.space_members (space_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_tasks_space_id ON public.tasks (space_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_tasks_status ON public.tasks (status);
|
||||
CREATE INDEX IF NOT EXISTS idx_tasks_claimed_by ON public.tasks (claimed_by);
|
||||
CREATE INDEX IF NOT EXISTS idx_tasks_assigned_to ON public.tasks (assigned_to);
|
||||
CREATE INDEX IF NOT EXISTS idx_tasks_project_id ON public.tasks (project_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_projects_space_id ON public.projects (space_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_payments_space_id ON public.payments (space_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_payments_member_id ON public.payments (member_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_payments_external_id ON public.payments (external_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_payments_link_status ON public.payments (link_status);
|
||||
CREATE INDEX IF NOT EXISTS idx_payments_transaction_date ON public.payments (transaction_date DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_contacts_space_id ON public.contacts (space_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_knowledge_base_space_id ON public.knowledge_base (space_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_secrets_space_id ON public.secrets (space_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_area_leads_space_id ON public.area_leads (space_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_integrations_space_id ON public.integrations (space_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_comms_channels_space_id ON public.comms_channels (space_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_comms_messages_channel ON public.comms_messages (channel_id, created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_comms_messages_space ON public.comms_messages (space_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_comms_messages_user ON public.comms_messages (user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_activity_log_space_id ON public.activity_log (space_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_activity_log_created_at ON public.activity_log (created_at DESC);
|
||||
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 5. HELPER FUNCTIONS
|
||||
-- Must come AFTER tables because they reference space_members.
|
||||
-- Both are SECURITY DEFINER with fixed search_path.
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- Must come after tables — they reference public.space_members.
|
||||
-- SECURITY DEFINER with fixed search_path prevents privilege escalation.
|
||||
-- -----------------------------------------------------------------------------
|
||||
|
||||
-- Returns all space_ids a given user belongs to.
|
||||
-- Used in RLS SELECT policies to avoid recursive policy evaluation.
|
||||
-- Returns all space_ids the given user belongs to.
|
||||
-- Used in RLS SELECT policies to avoid recursive evaluation.
|
||||
CREATE OR REPLACE FUNCTION public.get_user_space_ids(uid uuid)
|
||||
RETURNS SETOF uuid
|
||||
LANGUAGE sql STABLE SECURITY DEFINER
|
||||
@@ -336,8 +367,7 @@ AS $$
|
||||
SELECT space_id FROM public.space_members WHERE user_id = uid;
|
||||
$$;
|
||||
|
||||
-- Returns true if the user has one of the allowed roles in the given space.
|
||||
-- Used in RLS policies that require elevated permissions.
|
||||
-- Returns true if the user holds one of the given roles in the given space.
|
||||
CREATE OR REPLACE FUNCTION public.user_has_role_in_space(uid uuid, sid uuid, allowed_roles text[])
|
||||
RETURNS boolean
|
||||
LANGUAGE sql STABLE SECURITY DEFINER
|
||||
@@ -345,21 +375,16 @@ CREATE OR REPLACE FUNCTION public.user_has_role_in_space(uid uuid, sid uuid, all
|
||||
AS $$
|
||||
SELECT EXISTS (
|
||||
SELECT 1 FROM public.space_members
|
||||
WHERE user_id = uid AND space_id = sid AND role::text = ANY(allowed_roles)
|
||||
WHERE user_id = uid
|
||||
AND space_id = sid
|
||||
AND role::text = ANY(allowed_roles)
|
||||
);
|
||||
$$;
|
||||
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 6. ROW LEVEL SECURITY
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- Pattern:
|
||||
-- SELECT → any space member
|
||||
-- INSERT → any space member, or elevated role depending on sensitivity
|
||||
-- UPDATE → space member (own row) or elevated role
|
||||
-- DELETE → admin/board only
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
ALTER TABLE public.spaces ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE public.space_members ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE public.tasks ENABLE ROW LEVEL SECURITY;
|
||||
@@ -374,90 +399,73 @@ ALTER TABLE public.comms_channels ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE public.comms_messages ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE public.activity_log ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- ── spaces ───────────────────────────────────────────────────────────────────
|
||||
CREATE POLICY "spaces_select" ON public.spaces
|
||||
FOR SELECT USING (id IN (SELECT public.get_user_space_ids(auth.uid())));
|
||||
-- spaces
|
||||
CREATE POLICY spaces_select ON public.spaces FOR SELECT
|
||||
USING (id IN (SELECT public.get_user_space_ids(auth.uid())));
|
||||
CREATE POLICY spaces_insert ON public.spaces FOR INSERT
|
||||
WITH CHECK (true);
|
||||
CREATE POLICY spaces_update ON public.spaces FOR UPDATE
|
||||
USING (public.user_has_role_in_space(auth.uid(), id, ARRAY['admin','board']));
|
||||
|
||||
CREATE POLICY "spaces_insert" ON public.spaces
|
||||
FOR INSERT WITH CHECK (true); -- auth hook creates the space; server-side admin client used
|
||||
|
||||
CREATE POLICY "spaces_update" ON public.spaces
|
||||
FOR UPDATE USING (public.user_has_role_in_space(auth.uid(), id, ARRAY['admin','board']));
|
||||
|
||||
-- ── space_members ─────────────────────────────────────────────────────────────
|
||||
CREATE POLICY "members_select" ON public.space_members
|
||||
FOR SELECT USING (space_id IN (SELECT public.get_user_space_ids(auth.uid())));
|
||||
|
||||
CREATE POLICY "members_insert" ON public.space_members
|
||||
FOR INSERT WITH CHECK (
|
||||
-- space_members
|
||||
CREATE POLICY members_select ON public.space_members FOR SELECT
|
||||
USING (space_id IN (SELECT public.get_user_space_ids(auth.uid())));
|
||||
CREATE POLICY members_insert ON public.space_members FOR INSERT
|
||||
WITH CHECK (
|
||||
user_id = auth.uid()
|
||||
OR public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board','treasurer'])
|
||||
);
|
||||
|
||||
CREATE POLICY "members_update" ON public.space_members
|
||||
FOR UPDATE USING (
|
||||
CREATE POLICY members_update ON public.space_members FOR UPDATE
|
||||
USING (
|
||||
user_id = auth.uid()
|
||||
OR public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board','treasurer'])
|
||||
);
|
||||
CREATE POLICY members_delete ON public.space_members FOR DELETE
|
||||
USING (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin']));
|
||||
|
||||
CREATE POLICY "members_delete" ON public.space_members
|
||||
FOR DELETE USING (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin']));
|
||||
-- tasks
|
||||
CREATE POLICY tasks_select ON public.tasks FOR SELECT
|
||||
USING (space_id IN (SELECT public.get_user_space_ids(auth.uid())));
|
||||
CREATE POLICY tasks_insert ON public.tasks FOR INSERT
|
||||
WITH CHECK (space_id IN (SELECT public.get_user_space_ids(auth.uid())));
|
||||
CREATE POLICY tasks_update ON public.tasks FOR UPDATE
|
||||
USING (space_id IN (SELECT public.get_user_space_ids(auth.uid())));
|
||||
CREATE POLICY tasks_delete ON public.tasks FOR DELETE
|
||||
USING (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board']));
|
||||
|
||||
-- ── tasks ────────────────────────────────────────────────────────────────────
|
||||
CREATE POLICY "tasks_select" ON public.tasks
|
||||
FOR SELECT USING (space_id IN (SELECT public.get_user_space_ids(auth.uid())));
|
||||
-- projects
|
||||
CREATE POLICY projects_select ON public.projects FOR SELECT
|
||||
USING (space_id IN (SELECT public.get_user_space_ids(auth.uid())));
|
||||
CREATE POLICY projects_insert ON public.projects FOR INSERT
|
||||
WITH CHECK (space_id IN (SELECT public.get_user_space_ids(auth.uid())));
|
||||
CREATE POLICY projects_update ON public.projects FOR UPDATE
|
||||
USING (space_id IN (SELECT public.get_user_space_ids(auth.uid())));
|
||||
CREATE POLICY projects_delete ON public.projects FOR DELETE
|
||||
USING (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board']));
|
||||
|
||||
CREATE POLICY "tasks_insert" ON public.tasks
|
||||
FOR INSERT WITH CHECK (space_id IN (SELECT public.get_user_space_ids(auth.uid())));
|
||||
-- payments (treasurer / board / admin only)
|
||||
CREATE POLICY payments_select ON public.payments FOR SELECT
|
||||
USING (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board','treasurer']));
|
||||
CREATE POLICY payments_insert ON public.payments FOR INSERT
|
||||
WITH CHECK (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board','treasurer']));
|
||||
CREATE POLICY payments_update ON public.payments FOR UPDATE
|
||||
USING (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board','treasurer']));
|
||||
CREATE POLICY payments_delete ON public.payments FOR DELETE
|
||||
USING (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','treasurer']));
|
||||
|
||||
CREATE POLICY "tasks_update" ON public.tasks
|
||||
FOR UPDATE USING (space_id IN (SELECT public.get_user_space_ids(auth.uid())));
|
||||
-- contacts
|
||||
CREATE POLICY contacts_select ON public.contacts FOR SELECT
|
||||
USING (space_id IN (SELECT public.get_user_space_ids(auth.uid())));
|
||||
CREATE POLICY contacts_insert ON public.contacts FOR INSERT
|
||||
WITH CHECK (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board','treasurer']));
|
||||
CREATE POLICY contacts_update ON public.contacts FOR UPDATE
|
||||
USING (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board','treasurer']));
|
||||
CREATE POLICY contacts_delete ON public.contacts FOR DELETE
|
||||
USING (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board']));
|
||||
|
||||
CREATE POLICY "tasks_delete" ON public.tasks
|
||||
FOR DELETE USING (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board']));
|
||||
|
||||
-- ── projects ─────────────────────────────────────────────────────────────────
|
||||
CREATE POLICY "projects_select" ON public.projects
|
||||
FOR SELECT USING (space_id IN (SELECT public.get_user_space_ids(auth.uid())));
|
||||
|
||||
CREATE POLICY "projects_insert" ON public.projects
|
||||
FOR INSERT WITH CHECK (space_id IN (SELECT public.get_user_space_ids(auth.uid())));
|
||||
|
||||
CREATE POLICY "projects_update" ON public.projects
|
||||
FOR UPDATE USING (space_id IN (SELECT public.get_user_space_ids(auth.uid())));
|
||||
|
||||
CREATE POLICY "projects_delete" ON public.projects
|
||||
FOR DELETE USING (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board']));
|
||||
|
||||
-- ── payments ─────────────────────────────────────────────────────────────────
|
||||
CREATE POLICY "payments_select" ON public.payments
|
||||
FOR SELECT USING (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board','treasurer']));
|
||||
|
||||
CREATE POLICY "payments_insert" ON public.payments
|
||||
FOR INSERT WITH CHECK (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board','treasurer']));
|
||||
|
||||
CREATE POLICY "payments_update" ON public.payments
|
||||
FOR UPDATE USING (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board','treasurer']));
|
||||
|
||||
CREATE POLICY "payments_delete" ON public.payments
|
||||
FOR DELETE USING (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','treasurer']));
|
||||
|
||||
-- ── contacts ─────────────────────────────────────────────────────────────────
|
||||
CREATE POLICY "contacts_select" ON public.contacts
|
||||
FOR SELECT USING (space_id IN (SELECT public.get_user_space_ids(auth.uid())));
|
||||
|
||||
CREATE POLICY "contacts_insert" ON public.contacts
|
||||
FOR INSERT WITH CHECK (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board','treasurer']));
|
||||
|
||||
CREATE POLICY "contacts_update" ON public.contacts
|
||||
FOR UPDATE USING (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board','treasurer']));
|
||||
|
||||
CREATE POLICY "contacts_delete" ON public.contacts
|
||||
FOR DELETE USING (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board']));
|
||||
|
||||
-- ── knowledge_base ────────────────────────────────────────────────────────────
|
||||
CREATE POLICY "kb_select" ON public.knowledge_base
|
||||
FOR SELECT USING (
|
||||
-- knowledge_base
|
||||
CREATE POLICY kb_select ON public.knowledge_base FOR SELECT
|
||||
USING (
|
||||
space_id IN (SELECT public.get_user_space_ids(auth.uid()))
|
||||
AND (
|
||||
visibility = 'all_members'
|
||||
@@ -465,113 +473,95 @@ CREATE POLICY "kb_select" ON public.knowledge_base
|
||||
OR (visibility = 'admin_only' AND public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin']))
|
||||
)
|
||||
);
|
||||
CREATE POLICY kb_insert ON public.knowledge_base FOR INSERT
|
||||
WITH CHECK (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board','treasurer']));
|
||||
CREATE POLICY kb_update ON public.knowledge_base FOR UPDATE
|
||||
USING (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board','treasurer']));
|
||||
CREATE POLICY kb_delete ON public.knowledge_base FOR DELETE
|
||||
USING (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board']));
|
||||
|
||||
CREATE POLICY "kb_insert" ON public.knowledge_base
|
||||
FOR INSERT WITH CHECK (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board','treasurer']));
|
||||
-- secrets (board / admin only)
|
||||
CREATE POLICY secrets_select ON public.secrets FOR SELECT
|
||||
USING (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board']));
|
||||
CREATE POLICY secrets_insert ON public.secrets FOR INSERT
|
||||
WITH CHECK (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board']));
|
||||
CREATE POLICY secrets_update ON public.secrets FOR UPDATE
|
||||
USING (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board']));
|
||||
CREATE POLICY secrets_delete ON public.secrets FOR DELETE
|
||||
USING (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin']));
|
||||
|
||||
CREATE POLICY "kb_update" ON public.knowledge_base
|
||||
FOR UPDATE USING (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board','treasurer']));
|
||||
-- area_leads
|
||||
CREATE POLICY area_leads_select ON public.area_leads FOR SELECT
|
||||
USING (space_id IN (SELECT public.get_user_space_ids(auth.uid())));
|
||||
CREATE POLICY area_leads_insert ON public.area_leads FOR INSERT
|
||||
WITH CHECK (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board']));
|
||||
CREATE POLICY area_leads_update ON public.area_leads FOR UPDATE
|
||||
USING (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board']));
|
||||
CREATE POLICY area_leads_delete ON public.area_leads FOR DELETE
|
||||
USING (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin']));
|
||||
|
||||
CREATE POLICY "kb_delete" ON public.knowledge_base
|
||||
FOR DELETE USING (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board']));
|
||||
-- integrations (admin only)
|
||||
CREATE POLICY integrations_select ON public.integrations FOR SELECT
|
||||
USING (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board','treasurer']));
|
||||
CREATE POLICY integrations_insert ON public.integrations FOR INSERT
|
||||
WITH CHECK (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin']));
|
||||
CREATE POLICY integrations_update ON public.integrations FOR UPDATE
|
||||
USING (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin']));
|
||||
CREATE POLICY integrations_delete ON public.integrations FOR DELETE
|
||||
USING (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin']));
|
||||
|
||||
-- ── secrets ──────────────────────────────────────────────────────────────────
|
||||
CREATE POLICY "secrets_select" ON public.secrets
|
||||
FOR SELECT USING (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board']));
|
||||
-- comms_channels
|
||||
CREATE POLICY channels_select ON public.comms_channels FOR SELECT
|
||||
USING (space_id IN (SELECT public.get_user_space_ids(auth.uid())));
|
||||
CREATE POLICY channels_insert ON public.comms_channels FOR INSERT
|
||||
WITH CHECK (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board']));
|
||||
CREATE POLICY channels_update ON public.comms_channels FOR UPDATE
|
||||
USING (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board']));
|
||||
CREATE POLICY channels_delete ON public.comms_channels FOR DELETE
|
||||
USING (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin']));
|
||||
|
||||
CREATE POLICY "secrets_insert" ON public.secrets
|
||||
FOR INSERT WITH CHECK (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board']));
|
||||
|
||||
CREATE POLICY "secrets_update" ON public.secrets
|
||||
FOR UPDATE USING (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board']));
|
||||
|
||||
CREATE POLICY "secrets_delete" ON public.secrets
|
||||
FOR DELETE USING (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin']));
|
||||
|
||||
-- ── area_leads ───────────────────────────────────────────────────────────────
|
||||
CREATE POLICY "area_leads_select" ON public.area_leads
|
||||
FOR SELECT USING (space_id IN (SELECT public.get_user_space_ids(auth.uid())));
|
||||
|
||||
CREATE POLICY "area_leads_insert" ON public.area_leads
|
||||
FOR INSERT WITH CHECK (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board']));
|
||||
|
||||
CREATE POLICY "area_leads_update" ON public.area_leads
|
||||
FOR UPDATE USING (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board']));
|
||||
|
||||
CREATE POLICY "area_leads_delete" ON public.area_leads
|
||||
FOR DELETE USING (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin']));
|
||||
|
||||
-- ── integrations ─────────────────────────────────────────────────────────────
|
||||
CREATE POLICY "integrations_select" ON public.integrations
|
||||
FOR SELECT USING (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board','treasurer']));
|
||||
|
||||
CREATE POLICY "integrations_insert" ON public.integrations
|
||||
FOR INSERT WITH CHECK (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin']));
|
||||
|
||||
CREATE POLICY "integrations_update" ON public.integrations
|
||||
FOR UPDATE USING (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin']));
|
||||
|
||||
CREATE POLICY "integrations_delete" ON public.integrations
|
||||
FOR DELETE USING (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin']));
|
||||
|
||||
-- ── comms_channels ───────────────────────────────────────────────────────────
|
||||
CREATE POLICY "channels_select" ON public.comms_channels
|
||||
FOR SELECT USING (space_id IN (SELECT public.get_user_space_ids(auth.uid())));
|
||||
|
||||
CREATE POLICY "channels_insert" ON public.comms_channels
|
||||
FOR INSERT WITH CHECK (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board']));
|
||||
|
||||
CREATE POLICY "channels_update" ON public.comms_channels
|
||||
FOR UPDATE USING (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board']));
|
||||
|
||||
CREATE POLICY "channels_delete" ON public.comms_channels
|
||||
FOR DELETE USING (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin']));
|
||||
|
||||
-- ── comms_messages ───────────────────────────────────────────────────────────
|
||||
CREATE POLICY "messages_select" ON public.comms_messages
|
||||
FOR SELECT USING (space_id IN (SELECT public.get_user_space_ids(auth.uid())));
|
||||
|
||||
CREATE POLICY "messages_insert" ON public.comms_messages
|
||||
FOR INSERT WITH CHECK (
|
||||
-- comms_messages
|
||||
CREATE POLICY messages_select ON public.comms_messages FOR SELECT
|
||||
USING (space_id IN (SELECT public.get_user_space_ids(auth.uid())));
|
||||
CREATE POLICY messages_insert ON public.comms_messages FOR INSERT
|
||||
WITH CHECK (
|
||||
space_id IN (SELECT public.get_user_space_ids(auth.uid()))
|
||||
AND user_id = auth.uid()
|
||||
);
|
||||
|
||||
CREATE POLICY "messages_update" ON public.comms_messages
|
||||
FOR UPDATE USING (
|
||||
CREATE POLICY messages_update ON public.comms_messages FOR UPDATE
|
||||
USING (
|
||||
user_id = auth.uid()
|
||||
OR public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board'])
|
||||
);
|
||||
CREATE POLICY messages_delete ON public.comms_messages FOR DELETE
|
||||
USING (
|
||||
user_id = auth.uid()
|
||||
OR public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board'])
|
||||
);
|
||||
|
||||
CREATE POLICY "messages_delete" ON public.comms_messages
|
||||
FOR DELETE USING (
|
||||
user_id = auth.uid()
|
||||
OR public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board'])
|
||||
);
|
||||
|
||||
-- ── activity_log ─────────────────────────────────────────────────────────────
|
||||
CREATE POLICY "activity_select" ON public.activity_log
|
||||
FOR SELECT USING (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board']));
|
||||
|
||||
CREATE POLICY "activity_insert" ON public.activity_log
|
||||
FOR INSERT WITH CHECK (space_id IN (SELECT public.get_user_space_ids(auth.uid())));
|
||||
-- activity_log (append-only for members, readable by admin/board)
|
||||
CREATE POLICY activity_select ON public.activity_log FOR SELECT
|
||||
USING (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board']));
|
||||
CREATE POLICY activity_insert ON public.activity_log FOR INSERT
|
||||
WITH CHECK (space_id IN (SELECT public.get_user_space_ids(auth.uid())));
|
||||
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 7. TRIGGERS & AUTH HOOK
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- -----------------------------------------------------------------------------
|
||||
|
||||
-- Create default comms channels when a new space is created.
|
||||
-- Auto-create default channels when a new space is created
|
||||
CREATE OR REPLACE FUNCTION public.create_default_channels()
|
||||
RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql SECURITY DEFINER
|
||||
SET search_path = public
|
||||
AS $$
|
||||
BEGIN
|
||||
INSERT INTO public.comms_channels (space_id, name, icon, channel_type)
|
||||
INSERT INTO public.comms_channels (space_id, name, description, channel_type, is_default)
|
||||
VALUES
|
||||
(NEW.id, 'general', '💬', 'general'),
|
||||
(NEW.id, 'announcements', '📢', 'general'),
|
||||
(NEW.id, 'ops', '⚙️', 'ops');
|
||||
(NEW.id, 'general', 'General discussion', 'general', true),
|
||||
(NEW.id, 'announcements', 'Important announcements', 'general', false),
|
||||
(NEW.id, 'ops', 'Operations & logistics', 'ops', false);
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
@@ -581,37 +571,44 @@ CREATE TRIGGER on_space_created
|
||||
AFTER INSERT ON public.spaces
|
||||
FOR EACH ROW EXECUTE FUNCTION public.create_default_channels();
|
||||
|
||||
-- Auth hook: create a space_member row when a new user signs up.
|
||||
-- Reads metadata fields: space_id, role, full_name / display_name.
|
||||
-- NOTE: The app's signup flow calls createSpace/joinSpace server actions
|
||||
-- directly after signUp() returns a session (email confirm disabled).
|
||||
-- This trigger is a safety net for when email confirmation IS enabled.
|
||||
-- Auth signup hook
|
||||
-- Called when a new user is created in auth.users.
|
||||
-- Reads metadata from supabase.auth.signUp({ data: { space_id, role, display_name } })
|
||||
-- and inserts the corresponding space_members row.
|
||||
CREATE OR REPLACE FUNCTION public.handle_space_signup()
|
||||
RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql SECURITY DEFINER
|
||||
SET search_path = public
|
||||
AS $$
|
||||
DECLARE
|
||||
v_space_id uuid;
|
||||
v_role public.member_role;
|
||||
v_space_id uuid;
|
||||
v_role public.member_role;
|
||||
v_display_name text;
|
||||
BEGIN
|
||||
v_space_id := (NEW.raw_user_meta_data->>'space_id')::uuid;
|
||||
v_role := COALESCE(
|
||||
NULLIF((NEW.raw_user_meta_data->>'role'), '')::public.member_role,
|
||||
'member'
|
||||
);
|
||||
v_display_name := COALESCE(
|
||||
NULLIF(NEW.raw_user_meta_data->>'display_name', ''),
|
||||
NULLIF(NEW.raw_user_meta_data->>'full_name', ''),
|
||||
NEW.email
|
||||
);
|
||||
v_space_id := (NEW.raw_user_meta_data->>'space_id')::uuid;
|
||||
|
||||
-- Default to 'member' if role is missing or invalid
|
||||
v_role := COALESCE(
|
||||
NULLIF(NEW.raw_user_meta_data->>'role', '')::public.member_role,
|
||||
'member'
|
||||
);
|
||||
|
||||
-- Prefer display_name, fall back to full_name, then email
|
||||
v_display_name := COALESCE(
|
||||
NULLIF(NEW.raw_user_meta_data->>'display_name', ''),
|
||||
NULLIF(NEW.raw_user_meta_data->>'full_name', ''),
|
||||
NEW.email
|
||||
);
|
||||
|
||||
-- No space_id means this is not a hackerspace signup flow
|
||||
IF v_space_id IS NULL THEN
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
|
||||
INSERT INTO public.space_members (space_id, user_id, role, status, display_name, email, approved)
|
||||
VALUES (v_space_id, NEW.id, v_role, 'unverified', v_display_name, NEW.email, true)
|
||||
INSERT INTO public.space_members
|
||||
(space_id, user_id, role, status, display_name, email, approved)
|
||||
VALUES
|
||||
(v_space_id, NEW.id, v_role, 'unverified', v_display_name, NEW.email, true)
|
||||
ON CONFLICT (space_id, user_id) DO NOTHING;
|
||||
|
||||
RETURN NEW;
|
||||
@@ -624,9 +621,8 @@ CREATE TRIGGER on_auth_user_created
|
||||
FOR EACH ROW EXECUTE FUNCTION public.handle_space_signup();
|
||||
|
||||
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 8. REALTIME
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
-- Enable realtime for live chat.
|
||||
-- -----------------------------------------------------------------------------
|
||||
ALTER PUBLICATION supabase_realtime ADD TABLE public.comms_messages;
|
||||
ALTER PUBLICATION supabase_realtime ADD TABLE public.comms_channels;
|
||||
|
||||
Reference in New Issue
Block a user