feat: resolve schema discrepancies and update schema.sql
Fix column and constraint mismatches, and update schema.sql to match live DB. Co-authored-by: Moheeb Zara <1184415+virgilvox@users.noreply.github.com>
This commit is contained in:
+1
-1
@@ -62,7 +62,7 @@ export async function createTask(formData: {
|
|||||||
space_id: member.space_id,
|
space_id: member.space_id,
|
||||||
title: formData.title,
|
title: formData.title,
|
||||||
description: formData.description,
|
description: formData.description,
|
||||||
type: formData.type || 'task',
|
task_type: formData.type || 'task',
|
||||||
area: formData.area,
|
area: formData.area,
|
||||||
recurrence: formData.recurrence || 'none',
|
recurrence: formData.recurrence || 'none',
|
||||||
due_date: formData.due_date || null,
|
due_date: formData.due_date || null,
|
||||||
|
|||||||
+300
-401
@@ -1,20 +1,18 @@
|
|||||||
-- =============================================================================
|
-- =============================================================================
|
||||||
-- HACKERSPACE.SH — CANONICAL FRESH DEPLOYMENT SCHEMA
|
-- HACKERSPACE.SH — CANONICAL SCHEMA (single source of truth)
|
||||||
-- =============================================================================
|
-- =============================================================================
|
||||||
-- This is the single source of truth for a fresh database setup.
|
-- Run this file ONCE on a brand-new Supabase project.
|
||||||
-- Run this ONCE on a brand new Supabase project. It is fully idempotent.
|
-- It is fully idempotent: safe to run again if interrupted.
|
||||||
--
|
|
||||||
-- Replaces all previous scripts (000–011). Those are kept as history only.
|
|
||||||
--
|
--
|
||||||
-- Execution order:
|
-- Execution order:
|
||||||
-- 1. Extensions
|
-- 1. Extensions
|
||||||
-- 2. Enums
|
-- 2. Enums
|
||||||
-- 3. Helper functions (used in RLS policies)
|
-- 3. Tables (spaces → space_members → everything else)
|
||||||
-- 4. Tables (dependency order: spaces → space_members → everything else)
|
-- 4. Unique constraints & indexes
|
||||||
-- 5. Indexes
|
-- 5. Helper functions (reference space_members → must come after tables)
|
||||||
-- 6. Row Level Security (RLS) policies
|
-- 6. Row Level Security
|
||||||
-- 7. Triggers
|
-- 7. Triggers & auth hook
|
||||||
-- 8. Auth hook (Supabase auth.users trigger)
|
-- 8. Realtime
|
||||||
-- =============================================================================
|
-- =============================================================================
|
||||||
|
|
||||||
-- ─────────────────────────────────────────────────────────────────────────────
|
-- ─────────────────────────────────────────────────────────────────────────────
|
||||||
@@ -26,124 +24,26 @@ CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
|||||||
-- ─────────────────────────────────────────────────────────────────────────────
|
-- ─────────────────────────────────────────────────────────────────────────────
|
||||||
-- 2. ENUMS
|
-- 2. ENUMS
|
||||||
-- ─────────────────────────────────────────────────────────────────────────────
|
-- ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
DO $$ BEGIN CREATE TYPE public.member_role AS ENUM ('admin','board','treasurer','member','associate'); EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
-- Member role within a space
|
DO $$ BEGIN CREATE TYPE public.member_tier AS ENUM ('plus','basic','associate'); EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
DO $$ BEGIN
|
DO $$ BEGIN CREATE TYPE public.member_status AS ENUM ('current','late','inactive','unverified'); EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
CREATE TYPE public.member_role AS ENUM ('admin', 'board', 'treasurer', 'member', 'associate');
|
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 $$;
|
||||||
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 $$;
|
||||||
-- Membership tier
|
DO $$ BEGIN CREATE TYPE public.project_status AS ENUM ('backlog','in_progress','review','done','blocked'); EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
DO $$ BEGIN
|
DO $$ BEGIN CREATE TYPE public.payment_platform AS ENUM ('paypal','zeffy','venmo','cash'); EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
CREATE TYPE public.member_tier AS ENUM ('plus', 'basic', 'associate');
|
DO $$ BEGIN CREATE TYPE public.payment_link_status AS ENUM ('linked','unlinked'); EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
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 $$;
|
||||||
-- Membership lifecycle status
|
DO $$ BEGIN CREATE TYPE public.contact_type AS ENUM ('vendor','supplier','partner','landlord','city'); EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
DO $$ BEGIN
|
DO $$ BEGIN CREATE TYPE public.channel_type AS ENUM ('general','area','ops','project'); EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||||
CREATE TYPE public.member_status AS ENUM ('current', 'late', 'inactive', 'unverified');
|
|
||||||
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
|
||||||
|
|
||||||
-- Task completion status
|
|
||||||
-- NOTE: Both 'completed' AND 'done' are valid — 'done' is legacy, 'completed'
|
|
||||||
-- is canonical. Application code must treat both as finished.
|
|
||||||
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 $$;
|
|
||||||
|
|
||||||
-- Task category
|
|
||||||
DO $$ BEGIN
|
|
||||||
CREATE TYPE public.task_type AS ENUM ('chore', 'task');
|
|
||||||
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
|
||||||
|
|
||||||
-- Task / chore recurrence
|
|
||||||
DO $$ BEGIN
|
|
||||||
CREATE TYPE public.recurrence_type AS ENUM ('daily', 'weekly', 'biweekly', 'monthly', 'none');
|
|
||||||
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
|
||||||
|
|
||||||
-- Project lifecycle status
|
|
||||||
DO $$ BEGIN
|
|
||||||
CREATE TYPE public.project_status AS ENUM ('backlog', 'in_progress', 'review', 'done', 'blocked');
|
|
||||||
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
|
||||||
|
|
||||||
-- Payment platform source
|
|
||||||
DO $$ BEGIN
|
|
||||||
CREATE TYPE public.payment_platform AS ENUM ('paypal', 'zeffy', 'venmo', 'cash');
|
|
||||||
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
|
||||||
|
|
||||||
-- Whether a payment row has been matched to a member
|
|
||||||
DO $$ BEGIN
|
|
||||||
CREATE TYPE public.payment_link_status AS ENUM ('linked', 'unlinked');
|
|
||||||
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
|
||||||
|
|
||||||
-- Knowledge base article visibility
|
|
||||||
DO $$ BEGIN
|
|
||||||
CREATE TYPE public.kb_visibility AS ENUM ('all_members', 'board', 'admin_only');
|
|
||||||
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
|
||||||
|
|
||||||
-- Area lead assignment status
|
|
||||||
DO $$ BEGIN
|
|
||||||
CREATE TYPE public.area_lead_status AS ENUM ('active', 'vacant', 'handoff');
|
|
||||||
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
|
||||||
|
|
||||||
-- Contact / vendor type
|
|
||||||
DO $$ BEGIN
|
|
||||||
CREATE TYPE public.contact_type AS ENUM ('vendor', 'supplier', 'partner', 'landlord', 'city');
|
|
||||||
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
|
||||||
|
|
||||||
-- Chat channel category
|
|
||||||
DO $$ BEGIN
|
|
||||||
CREATE TYPE public.channel_type AS ENUM ('general', 'area', 'ops', 'project');
|
|
||||||
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
|
||||||
|
|
||||||
|
|
||||||
-- ─────────────────────────────────────────────────────────────────────────────
|
-- ─────────────────────────────────────────────────────────────────────────────
|
||||||
-- 3. HELPER FUNCTIONS
|
-- 3. TABLES (dependency order)
|
||||||
-- Must be created before tables so they can be referenced in RLS policies.
|
|
||||||
-- Both are SECURITY DEFINER + fixed search_path to prevent privilege escalation.
|
|
||||||
-- ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
-- Returns all space_ids a given user belongs to.
|
|
||||||
-- Used in RLS policies to avoid recursive policy checks on space_members.
|
|
||||||
CREATE OR REPLACE FUNCTION public.get_user_space_ids(uid uuid)
|
|
||||||
RETURNS SETOF uuid
|
|
||||||
LANGUAGE sql
|
|
||||||
STABLE
|
|
||||||
SECURITY DEFINER
|
|
||||||
SET search_path = public
|
|
||||||
AS $$
|
|
||||||
SELECT space_id FROM public.space_members WHERE user_id = uid;
|
|
||||||
$$;
|
|
||||||
|
|
||||||
-- Returns true if a user has one of the allowed roles in a specific space.
|
|
||||||
-- Used in RLS policies that require elevated permissions (admin, board, etc.).
|
|
||||||
CREATE OR REPLACE FUNCTION public.user_has_role_in_space(
|
|
||||||
uid uuid,
|
|
||||||
sid uuid,
|
|
||||||
allowed_roles text[]
|
|
||||||
)
|
|
||||||
RETURNS boolean
|
|
||||||
LANGUAGE sql
|
|
||||||
STABLE
|
|
||||||
SECURITY DEFINER
|
|
||||||
SET search_path = public
|
|
||||||
AS $$
|
|
||||||
SELECT EXISTS (
|
|
||||||
SELECT 1 FROM public.space_members
|
|
||||||
WHERE user_id = uid
|
|
||||||
AND space_id = sid
|
|
||||||
AND role::text = ANY(allowed_roles)
|
|
||||||
);
|
|
||||||
$$;
|
|
||||||
|
|
||||||
|
|
||||||
-- ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
-- 4. TABLES
|
|
||||||
-- ─────────────────────────────────────────────────────────────────────────────
|
-- ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
-- ── spaces ───────────────────────────────────────────────────────────────────
|
-- ── spaces ───────────────────────────────────────────────────────────────────
|
||||||
-- Top-level tenant. Each hackerspace is one row.
|
|
||||||
CREATE TABLE IF NOT EXISTS public.spaces (
|
CREATE TABLE IF NOT EXISTS public.spaces (
|
||||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
name text NOT NULL,
|
name text NOT NULL,
|
||||||
@@ -152,32 +52,39 @@ CREATE TABLE IF NOT EXISTS public.spaces (
|
|||||||
logo_url text,
|
logo_url text,
|
||||||
city text,
|
city text,
|
||||||
address text,
|
address text,
|
||||||
timezone text DEFAULT 'America/Phoenix',
|
timezone text NOT NULL DEFAULT 'America/Phoenix',
|
||||||
invite_code text,
|
invite_code text,
|
||||||
require_approval boolean NOT NULL DEFAULT true,
|
require_approval boolean NOT NULL DEFAULT true,
|
||||||
public_member_directory boolean NOT NULL DEFAULT false,
|
public_member_directory boolean NOT NULL DEFAULT false,
|
||||||
webhook_secret text,
|
webhook_secret text,
|
||||||
settings jsonb DEFAULT '{}',
|
settings jsonb NOT NULL DEFAULT '{}',
|
||||||
created_at timestamptz NOT NULL DEFAULT now(),
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
updated_at timestamptz NOT NULL DEFAULT now()
|
updated_at timestamptz NOT NULL DEFAULT now()
|
||||||
);
|
);
|
||||||
|
|
||||||
-- ── space_members ─────────────────────────────────────────────────────────────
|
-- ── space_members ─────────────────────────────────────────────────────────────
|
||||||
-- One row per (user × space). A user may belong to multiple spaces.
|
-- 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)
|
||||||
CREATE TABLE IF NOT EXISTS public.space_members (
|
CREATE TABLE IF NOT EXISTS public.space_members (
|
||||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
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,
|
||||||
user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
user_id uuid REFERENCES auth.users(id) ON DELETE SET NULL,
|
||||||
role public.member_role NOT NULL DEFAULT 'member',
|
role public.member_role NOT NULL DEFAULT 'member',
|
||||||
tier public.member_tier NOT NULL DEFAULT 'basic',
|
tier public.member_tier NOT NULL DEFAULT 'basic',
|
||||||
status public.member_status NOT NULL DEFAULT 'unverified',
|
status public.member_status NOT NULL DEFAULT 'unverified',
|
||||||
approved boolean NOT NULL DEFAULT true,
|
approved boolean NOT NULL DEFAULT true,
|
||||||
full_name text,
|
display_name text,
|
||||||
|
handle text,
|
||||||
email text,
|
email text,
|
||||||
phone text,
|
phone text,
|
||||||
bio text,
|
bio text,
|
||||||
avatar_url text,
|
avatar_url text,
|
||||||
joined_at timestamptz DEFAULT now(),
|
has_card_access boolean NOT NULL DEFAULT false,
|
||||||
|
payment_status text,
|
||||||
|
payment_note text,
|
||||||
|
last_paid_at timestamptz,
|
||||||
|
joined_at timestamptz NOT NULL DEFAULT now(),
|
||||||
dues_paid_until timestamptz,
|
dues_paid_until timestamptz,
|
||||||
stripe_customer_id text,
|
stripe_customer_id text,
|
||||||
created_at timestamptz NOT NULL DEFAULT now(),
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
@@ -185,77 +92,82 @@ CREATE TABLE IF NOT EXISTS public.space_members (
|
|||||||
UNIQUE (space_id, user_id)
|
UNIQUE (space_id, user_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
-- ── projects ─────────────────────────────────────────────────────────────────
|
||||||
|
-- Must be created before tasks (tasks.project_id FK).
|
||||||
|
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,
|
||||||
|
description text,
|
||||||
|
status public.project_status NOT NULL DEFAULT 'backlog',
|
||||||
|
area text,
|
||||||
|
tags 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()
|
||||||
|
);
|
||||||
|
|
||||||
-- ── tasks ────────────────────────────────────────────────────────────────────
|
-- ── tasks ────────────────────────────────────────────────────────────────────
|
||||||
|
-- task_type — the enum column (canonical)
|
||||||
|
-- status — task lifecycle; both 'completed' and 'done' mean finished
|
||||||
CREATE TABLE IF NOT EXISTS public.tasks (
|
CREATE TABLE IF NOT EXISTS public.tasks (
|
||||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
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,
|
title text NOT NULL,
|
||||||
description text,
|
description text,
|
||||||
type task_type NOT NULL DEFAULT 'task',
|
task_type public.task_type NOT NULL DEFAULT 'task',
|
||||||
task_type text NOT NULL DEFAULT 'chore', -- legacy text col, kept for compat
|
status public.task_status NOT NULL DEFAULT 'open',
|
||||||
status task_status NOT NULL DEFAULT 'open',
|
recurrence public.recurrence_type NOT NULL DEFAULT 'none',
|
||||||
|
priority text NOT NULL DEFAULT 'medium',
|
||||||
area text,
|
area text,
|
||||||
recurrence recurrence_type NOT NULL DEFAULT 'none',
|
project_id uuid REFERENCES public.projects(id) ON DELETE SET NULL,
|
||||||
|
tags text[],
|
||||||
due_date timestamptz,
|
due_date timestamptz,
|
||||||
assigned_to uuid REFERENCES public.space_members(id) ON DELETE SET NULL,
|
claimed_by uuid REFERENCES auth.users(id) ON DELETE SET NULL,
|
||||||
assigned_to_name text,
|
|
||||||
claimed_by uuid REFERENCES public.space_members(id) ON DELETE SET NULL,
|
|
||||||
claimed_by_name text,
|
claimed_by_name text,
|
||||||
requested_by uuid REFERENCES public.space_members(id) ON DELETE SET NULL,
|
assigned_to uuid REFERENCES auth.users(id) ON DELETE SET NULL,
|
||||||
|
assigned_to_name text,
|
||||||
|
requested_by uuid REFERENCES auth.users(id) ON DELETE SET NULL,
|
||||||
requested_by_name text,
|
requested_by_name text,
|
||||||
subtask_total integer NOT NULL DEFAULT 0,
|
subtask_total integer NOT NULL DEFAULT 0,
|
||||||
subtask_completed integer NOT NULL DEFAULT 0,
|
subtask_completed integer NOT NULL DEFAULT 0,
|
||||||
progress integer NOT NULL DEFAULT 0,
|
progress integer NOT NULL DEFAULT 0,
|
||||||
last_done_at timestamptz,
|
last_done_at timestamptz,
|
||||||
completed_at timestamptz,
|
completed_at timestamptz,
|
||||||
created_at timestamptz NOT NULL DEFAULT now(),
|
created_by uuid REFERENCES auth.users(id) ON DELETE SET NULL,
|
||||||
updated_at timestamptz NOT NULL DEFAULT now()
|
|
||||||
);
|
|
||||||
|
|
||||||
-- ── projects ─────────────────────────────────────────────────────────────────
|
|
||||||
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,
|
|
||||||
name text, -- legacy alias for title, kept for compat
|
|
||||||
description text,
|
|
||||||
status project_status NOT NULL DEFAULT 'backlog',
|
|
||||||
area text,
|
|
||||||
category text,
|
|
||||||
tags text[],
|
|
||||||
assignees text, -- free-form text list
|
|
||||||
assignee_names text[], -- structured array
|
|
||||||
task_count integer NOT NULL DEFAULT 0,
|
|
||||||
tasks_completed integer NOT NULL DEFAULT 0,
|
|
||||||
progress integer NOT NULL DEFAULT 0,
|
|
||||||
due_date timestamptz,
|
|
||||||
created_at timestamptz NOT NULL DEFAULT now(),
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
updated_at timestamptz NOT NULL DEFAULT now()
|
updated_at timestamptz NOT NULL DEFAULT now()
|
||||||
);
|
);
|
||||||
|
|
||||||
-- ── payments ─────────────────────────────────────────────────────────────────
|
-- ── payments ─────────────────────────────────────────────────────────────────
|
||||||
-- One row per transaction. Imported from PayPal/Zeffy/Venmo or logged as cash.
|
-- 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
|
||||||
CREATE TABLE IF NOT EXISTS public.payments (
|
CREATE TABLE IF NOT EXISTS public.payments (
|
||||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
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,
|
||||||
platform payment_platform NOT NULL,
|
platform public.payment_platform NOT NULL,
|
||||||
amount numeric(10, 2) NOT NULL,
|
amount numeric(10,2) NOT NULL,
|
||||||
from_identifier text, -- email, @handle, or name from source platform
|
from_identifier text,
|
||||||
from_note text, -- memo / note from sender
|
from_note text,
|
||||||
member_id uuid REFERENCES public.space_members(id) ON DELETE SET NULL,
|
member_id uuid REFERENCES public.space_members(id) ON DELETE SET NULL,
|
||||||
member_name text,
|
member_name text,
|
||||||
link_status payment_link_status NOT NULL DEFAULT 'unlinked',
|
link_status public.payment_link_status NOT NULL DEFAULT 'unlinked',
|
||||||
transaction_date timestamptz NOT NULL,
|
transaction_date timestamptz NOT NULL DEFAULT now(),
|
||||||
created_at timestamptz NOT NULL DEFAULT now()
|
created_at timestamptz NOT NULL DEFAULT now()
|
||||||
);
|
);
|
||||||
|
|
||||||
-- ── contacts ─────────────────────────────────────────────────────────────────
|
-- ── contacts ─────────────────────────────────────────────────────────────────
|
||||||
|
-- code — short auto-generated identifier (e.g. "VEN234")
|
||||||
CREATE TABLE IF NOT EXISTS public.contacts (
|
CREATE TABLE IF NOT EXISTS public.contacts (
|
||||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
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,
|
||||||
name text NOT NULL,
|
name text NOT NULL,
|
||||||
code text NOT NULL, -- short identifier
|
code text NOT NULL,
|
||||||
contact_type contact_type NOT NULL,
|
contact_type public.contact_type NOT NULL DEFAULT 'vendor',
|
||||||
email text,
|
email text,
|
||||||
phone text,
|
phone text,
|
||||||
details text,
|
details text,
|
||||||
@@ -267,6 +179,9 @@ CREATE TABLE IF NOT EXISTS public.contacts (
|
|||||||
);
|
);
|
||||||
|
|
||||||
-- ── knowledge_base ────────────────────────────────────────────────────────────
|
-- ── 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
|
||||||
CREATE TABLE IF NOT EXISTS public.knowledge_base (
|
CREATE TABLE IF NOT EXISTS public.knowledge_base (
|
||||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
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,
|
||||||
@@ -274,34 +189,33 @@ CREATE TABLE IF NOT EXISTS public.knowledge_base (
|
|||||||
description text,
|
description text,
|
||||||
content text,
|
content text,
|
||||||
icon text,
|
icon text,
|
||||||
visibility kb_visibility NOT NULL DEFAULT 'all_members',
|
visibility public.kb_visibility NOT NULL DEFAULT 'all_members',
|
||||||
access_level text NOT NULL DEFAULT 'all_members', -- legacy text col
|
|
||||||
area text,
|
area text,
|
||||||
tags text[],
|
tags text[],
|
||||||
is_pinned boolean NOT NULL DEFAULT false,
|
is_pinned boolean NOT NULL DEFAULT false,
|
||||||
pinned boolean NOT NULL DEFAULT false, -- legacy alias
|
|
||||||
updated_by_id uuid REFERENCES public.space_members(id) ON DELETE SET NULL,
|
updated_by_id uuid REFERENCES public.space_members(id) ON DELETE SET NULL,
|
||||||
updated_by_name text,
|
updated_by_name text,
|
||||||
updated_by text, -- legacy text col
|
|
||||||
created_at timestamptz NOT NULL DEFAULT now(),
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
updated_at timestamptz NOT NULL DEFAULT now()
|
updated_at timestamptz NOT NULL DEFAULT now()
|
||||||
);
|
);
|
||||||
|
|
||||||
-- ── secrets ──────────────────────────────────────────────────────────────────
|
-- ── secrets ──────────────────────────────────────────────────────────────────
|
||||||
-- Shared credentials vault. Only admins can write; board+ can read.
|
-- Shared credentials vault. Admin write, board+ read.
|
||||||
|
-- value stored as plain text — encryption is a future enhancement.
|
||||||
CREATE TABLE IF NOT EXISTS public.secrets (
|
CREATE TABLE IF NOT EXISTS public.secrets (
|
||||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
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,
|
title text NOT NULL,
|
||||||
icon text,
|
icon text,
|
||||||
description text,
|
description text,
|
||||||
value text NOT NULL, -- stored as plain text; encryption is a future enhancement
|
value text NOT NULL,
|
||||||
area text,
|
area text,
|
||||||
created_at timestamptz NOT NULL DEFAULT now(),
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
updated_at timestamptz NOT NULL DEFAULT now()
|
updated_at timestamptz NOT NULL DEFAULT now()
|
||||||
);
|
);
|
||||||
|
|
||||||
-- ── area_leads ───────────────────────────────────────────────────────────────
|
-- ── area_leads ───────────────────────────────────────────────────────────────
|
||||||
|
-- area_code must be unique within a space (used as upsert conflict target).
|
||||||
CREATE TABLE IF NOT EXISTS public.area_leads (
|
CREATE TABLE IF NOT EXISTS public.area_leads (
|
||||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
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,
|
||||||
@@ -309,14 +223,15 @@ CREATE TABLE IF NOT EXISTS public.area_leads (
|
|||||||
area_code text NOT NULL,
|
area_code text NOT NULL,
|
||||||
lead_id uuid REFERENCES public.space_members(id) ON DELETE SET NULL,
|
lead_id uuid REFERENCES public.space_members(id) ON DELETE SET NULL,
|
||||||
lead_handle text,
|
lead_handle text,
|
||||||
status area_lead_status NOT NULL DEFAULT 'active',
|
status public.area_lead_status NOT NULL DEFAULT 'active',
|
||||||
created_at timestamptz NOT NULL DEFAULT now(),
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
updated_at timestamptz NOT NULL DEFAULT now()
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
UNIQUE (space_id, area_code)
|
||||||
);
|
);
|
||||||
|
|
||||||
-- ── integrations ─────────────────────────────────────────────────────────────
|
-- ── integrations ─────────────────────────────────────────────────────────────
|
||||||
-- Payment platform credentials and other third-party configs.
|
-- config JSONB holds platform-specific credentials (e.g. client_id, token).
|
||||||
-- config JSONB stores platform-specific keys (e.g. { client_id, client_secret }).
|
-- platform must be unique within a space (used as upsert conflict target).
|
||||||
CREATE TABLE IF NOT EXISTS public.integrations (
|
CREATE TABLE IF NOT EXISTS public.integrations (
|
||||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
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,
|
||||||
@@ -331,13 +246,14 @@ CREATE TABLE IF NOT EXISTS public.integrations (
|
|||||||
);
|
);
|
||||||
|
|
||||||
-- ── comms_channels ───────────────────────────────────────────────────────────
|
-- ── comms_channels ───────────────────────────────────────────────────────────
|
||||||
|
-- name must be unique within a space.
|
||||||
CREATE TABLE IF NOT EXISTS public.comms_channels (
|
CREATE TABLE IF NOT EXISTS public.comms_channels (
|
||||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
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,
|
||||||
name text NOT NULL,
|
name text NOT NULL,
|
||||||
icon text,
|
icon text,
|
||||||
channel_type channel_type NOT NULL DEFAULT 'general',
|
channel_type public.channel_type NOT NULL DEFAULT 'general',
|
||||||
area_reference text, -- links an 'area' channel to an area code
|
area_reference text,
|
||||||
project_id uuid REFERENCES public.projects(id) ON DELETE SET NULL,
|
project_id uuid REFERENCES public.projects(id) ON DELETE SET NULL,
|
||||||
member_count integer NOT NULL DEFAULT 0,
|
member_count integer NOT NULL DEFAULT 0,
|
||||||
unread_count integer NOT NULL DEFAULT 0,
|
unread_count integer NOT NULL DEFAULT 0,
|
||||||
@@ -358,6 +274,7 @@ CREATE TABLE IF NOT EXISTS public.comms_messages (
|
|||||||
);
|
);
|
||||||
|
|
||||||
-- ── activity_log ─────────────────────────────────────────────────────────────
|
-- ── activity_log ─────────────────────────────────────────────────────────────
|
||||||
|
-- Append-only audit log. No UPDATE/DELETE policies.
|
||||||
CREATE TABLE IF NOT EXISTS public.activity_log (
|
CREATE TABLE IF NOT EXISTS public.activity_log (
|
||||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
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,
|
||||||
@@ -372,37 +289,75 @@ CREATE TABLE IF NOT EXISTS public.activity_log (
|
|||||||
|
|
||||||
|
|
||||||
-- ─────────────────────────────────────────────────────────────────────────────
|
-- ─────────────────────────────────────────────────────────────────────────────
|
||||||
-- 5. INDEXES
|
-- 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_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_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_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_status ON public.tasks (status);
|
||||||
CREATE INDEX IF NOT EXISTS idx_tasks_assigned_to ON public.tasks (assigned_to);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_tasks_claimed_by ON public.tasks (claimed_by);
|
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_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_space_id ON public.payments (space_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_payments_platform ON public.payments (platform);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_payments_link_status ON public.payments (link_status);
|
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_payments_transaction_date ON public.payments (transaction_date DESC);
|
||||||
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_kb_space_id ON public.knowledge_base (space_id);
|
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_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_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_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_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_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);
|
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.
|
||||||
|
-- ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
-- Returns all space_ids a given user belongs to.
|
||||||
|
-- Used in RLS SELECT policies to avoid recursive policy evaluation.
|
||||||
|
CREATE OR REPLACE FUNCTION public.get_user_space_ids(uid uuid)
|
||||||
|
RETURNS SETOF uuid
|
||||||
|
LANGUAGE sql STABLE SECURITY DEFINER
|
||||||
|
SET search_path = public
|
||||||
|
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.
|
||||||
|
CREATE OR REPLACE FUNCTION public.user_has_role_in_space(uid uuid, sid uuid, allowed_roles text[])
|
||||||
|
RETURNS boolean
|
||||||
|
LANGUAGE sql STABLE SECURITY DEFINER
|
||||||
|
SET search_path = public
|
||||||
|
AS $$
|
||||||
|
SELECT EXISTS (
|
||||||
|
SELECT 1 FROM public.space_members
|
||||||
|
WHERE user_id = uid AND space_id = sid AND role::text = ANY(allowed_roles)
|
||||||
|
);
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
-- ─────────────────────────────────────────────────────────────────────────────
|
-- ─────────────────────────────────────────────────────────────────────────────
|
||||||
-- 6. ROW LEVEL SECURITY
|
-- 6. ROW LEVEL SECURITY
|
||||||
-- ─────────────────────────────────────────────────────────────────────────────
|
-- ─────────────────────────────────────────────────────────────────────────────
|
||||||
-- Pattern: Every table has RLS enabled.
|
-- Pattern:
|
||||||
-- SELECT policies: any space member can read their own space's data.
|
-- SELECT → any space member
|
||||||
-- WRITE policies: elevated roles required (admin/board/treasurer as appropriate).
|
-- INSERT → any space member, or elevated role depending on sensitivity
|
||||||
-- The helper functions above prevent recursive policy evaluation.
|
-- UPDATE → space member (own row) or elevated role
|
||||||
|
-- DELETE → admin/board only
|
||||||
-- ─────────────────────────────────────────────────────────────────────────────
|
-- ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
ALTER TABLE public.spaces ENABLE ROW LEVEL SECURITY;
|
ALTER TABLE public.spaces ENABLE ROW LEVEL SECURITY;
|
||||||
@@ -420,227 +375,203 @@ ALTER TABLE public.comms_messages ENABLE ROW LEVEL SECURITY;
|
|||||||
ALTER TABLE public.activity_log ENABLE ROW LEVEL SECURITY;
|
ALTER TABLE public.activity_log ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
-- ── spaces ───────────────────────────────────────────────────────────────────
|
-- ── spaces ───────────────────────────────────────────────────────────────────
|
||||||
DROP POLICY IF EXISTS spaces_select_members ON public.spaces;
|
CREATE POLICY "spaces_select" ON public.spaces
|
||||||
DROP POLICY IF EXISTS spaces_insert_authenticated ON public.spaces;
|
FOR SELECT USING (id IN (SELECT public.get_user_space_ids(auth.uid())));
|
||||||
DROP POLICY IF EXISTS spaces_update_admins ON public.spaces;
|
|
||||||
DROP POLICY IF EXISTS spaces_delete_admins ON public.spaces;
|
|
||||||
|
|
||||||
CREATE POLICY spaces_select_members ON public.spaces
|
CREATE POLICY "spaces_insert" ON public.spaces
|
||||||
FOR SELECT USING (id IN (SELECT get_user_space_ids(auth.uid())));
|
FOR INSERT WITH CHECK (true); -- auth hook creates the space; server-side admin client used
|
||||||
|
|
||||||
CREATE POLICY spaces_insert_authenticated ON public.spaces
|
CREATE POLICY "spaces_update" ON public.spaces
|
||||||
FOR INSERT WITH CHECK (auth.uid() IS NOT NULL);
|
FOR UPDATE USING (public.user_has_role_in_space(auth.uid(), id, ARRAY['admin','board']));
|
||||||
|
|
||||||
CREATE POLICY spaces_update_admins ON public.spaces
|
|
||||||
FOR UPDATE USING (user_has_role_in_space(auth.uid(), id, ARRAY['admin']));
|
|
||||||
|
|
||||||
CREATE POLICY spaces_delete_admins ON public.spaces
|
|
||||||
FOR DELETE USING (user_has_role_in_space(auth.uid(), id, ARRAY['admin']));
|
|
||||||
|
|
||||||
-- ── space_members ─────────────────────────────────────────────────────────────
|
-- ── space_members ─────────────────────────────────────────────────────────────
|
||||||
DROP POLICY IF EXISTS space_members_select_own ON public.space_members;
|
CREATE POLICY "members_select" ON public.space_members
|
||||||
DROP POLICY IF EXISTS space_members_select_same_space ON public.space_members;
|
FOR SELECT USING (space_id IN (SELECT public.get_user_space_ids(auth.uid())));
|
||||||
DROP POLICY IF EXISTS space_members_insert_authenticated ON public.space_members;
|
|
||||||
DROP POLICY IF EXISTS space_members_update_admins ON public.space_members;
|
|
||||||
DROP POLICY IF EXISTS space_members_delete_admins ON public.space_members;
|
|
||||||
|
|
||||||
-- Own row always visible (needed for get_user_space_ids bootstrapping)
|
CREATE POLICY "members_insert" ON public.space_members
|
||||||
CREATE POLICY space_members_select_own ON public.space_members
|
|
||||||
FOR SELECT USING (user_id = auth.uid());
|
|
||||||
|
|
||||||
-- All members of the same space can see each other
|
|
||||||
CREATE POLICY space_members_select_same_space ON public.space_members
|
|
||||||
FOR SELECT USING (space_id IN (SELECT get_user_space_ids(auth.uid())));
|
|
||||||
|
|
||||||
-- A user may insert their own row, or admins/board may insert on behalf of others
|
|
||||||
CREATE POLICY space_members_insert_authenticated ON public.space_members
|
|
||||||
FOR INSERT WITH CHECK (
|
FOR INSERT WITH CHECK (
|
||||||
user_id = auth.uid()
|
user_id = auth.uid()
|
||||||
OR user_has_role_in_space(auth.uid(), space_id, ARRAY['admin', 'board'])
|
OR public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board','treasurer'])
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Members may update their own profile; admins may update anyone
|
CREATE POLICY "members_update" ON public.space_members
|
||||||
CREATE POLICY space_members_update_admins ON public.space_members
|
|
||||||
FOR UPDATE USING (
|
FOR UPDATE USING (
|
||||||
user_id = auth.uid()
|
user_id = auth.uid()
|
||||||
OR user_has_role_in_space(auth.uid(), space_id, ARRAY['admin'])
|
OR public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board','treasurer'])
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE POLICY space_members_delete_admins ON public.space_members
|
CREATE POLICY "members_delete" ON public.space_members
|
||||||
FOR DELETE USING (user_has_role_in_space(auth.uid(), space_id, ARRAY['admin']));
|
FOR DELETE USING (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin']));
|
||||||
|
|
||||||
-- ── tasks ────────────────────────────────────────────────────────────────────
|
-- ── tasks ────────────────────────────────────────────────────────────────────
|
||||||
DROP POLICY IF EXISTS tasks_select_members ON public.tasks;
|
CREATE POLICY "tasks_select" ON public.tasks
|
||||||
DROP POLICY IF EXISTS tasks_insert_members ON public.tasks;
|
FOR SELECT USING (space_id IN (SELECT public.get_user_space_ids(auth.uid())));
|
||||||
DROP POLICY IF EXISTS tasks_update_members ON public.tasks;
|
|
||||||
DROP POLICY IF EXISTS tasks_delete_members ON public.tasks;
|
|
||||||
|
|
||||||
CREATE POLICY tasks_select_members ON public.tasks
|
CREATE POLICY "tasks_insert" ON public.tasks
|
||||||
FOR SELECT USING (space_id IN (SELECT get_user_space_ids(auth.uid())));
|
FOR INSERT WITH CHECK (space_id IN (SELECT public.get_user_space_ids(auth.uid())));
|
||||||
|
|
||||||
CREATE POLICY tasks_insert_members ON public.tasks
|
CREATE POLICY "tasks_update" ON public.tasks
|
||||||
FOR INSERT WITH CHECK (space_id IN (SELECT get_user_space_ids(auth.uid())));
|
FOR UPDATE USING (space_id IN (SELECT public.get_user_space_ids(auth.uid())));
|
||||||
|
|
||||||
CREATE POLICY tasks_update_members ON public.tasks
|
CREATE POLICY "tasks_delete" ON public.tasks
|
||||||
FOR UPDATE USING (space_id IN (SELECT get_user_space_ids(auth.uid())));
|
FOR DELETE USING (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board']));
|
||||||
|
|
||||||
CREATE POLICY tasks_delete_members ON public.tasks
|
|
||||||
FOR DELETE USING (space_id IN (SELECT get_user_space_ids(auth.uid())));
|
|
||||||
|
|
||||||
-- ── projects ─────────────────────────────────────────────────────────────────
|
-- ── projects ─────────────────────────────────────────────────────────────────
|
||||||
DROP POLICY IF EXISTS projects_select_members ON public.projects;
|
CREATE POLICY "projects_select" ON public.projects
|
||||||
DROP POLICY IF EXISTS projects_insert_members ON public.projects;
|
FOR SELECT USING (space_id IN (SELECT public.get_user_space_ids(auth.uid())));
|
||||||
DROP POLICY IF EXISTS projects_update_members ON public.projects;
|
|
||||||
DROP POLICY IF EXISTS projects_delete_members ON public.projects;
|
|
||||||
|
|
||||||
CREATE POLICY projects_select_members ON public.projects
|
CREATE POLICY "projects_insert" ON public.projects
|
||||||
FOR SELECT USING (space_id IN (SELECT get_user_space_ids(auth.uid())));
|
FOR INSERT WITH CHECK (space_id IN (SELECT public.get_user_space_ids(auth.uid())));
|
||||||
|
|
||||||
CREATE POLICY projects_insert_members ON public.projects
|
CREATE POLICY "projects_update" ON public.projects
|
||||||
FOR INSERT WITH CHECK (space_id IN (SELECT get_user_space_ids(auth.uid())));
|
FOR UPDATE USING (space_id IN (SELECT public.get_user_space_ids(auth.uid())));
|
||||||
|
|
||||||
CREATE POLICY projects_update_members ON public.projects
|
CREATE POLICY "projects_delete" ON public.projects
|
||||||
FOR UPDATE USING (space_id IN (SELECT get_user_space_ids(auth.uid())));
|
FOR DELETE USING (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board']));
|
||||||
|
|
||||||
CREATE POLICY projects_delete_members ON public.projects
|
|
||||||
FOR DELETE USING (space_id IN (SELECT get_user_space_ids(auth.uid())));
|
|
||||||
|
|
||||||
-- ── payments ─────────────────────────────────────────────────────────────────
|
-- ── payments ─────────────────────────────────────────────────────────────────
|
||||||
DROP POLICY IF EXISTS payments_select_treasurer ON public.payments;
|
CREATE POLICY "payments_select" ON public.payments
|
||||||
DROP POLICY IF EXISTS payments_modify_treasurer ON public.payments;
|
FOR SELECT USING (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board','treasurer']));
|
||||||
|
|
||||||
CREATE POLICY payments_select_treasurer ON public.payments
|
CREATE POLICY "payments_insert" ON public.payments
|
||||||
FOR SELECT USING (
|
FOR INSERT WITH CHECK (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board','treasurer']));
|
||||||
user_has_role_in_space(auth.uid(), space_id, ARRAY['admin', 'board', 'treasurer'])
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE POLICY payments_modify_treasurer ON public.payments
|
CREATE POLICY "payments_update" ON public.payments
|
||||||
FOR ALL USING (
|
FOR UPDATE USING (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board','treasurer']));
|
||||||
user_has_role_in_space(auth.uid(), space_id, ARRAY['admin', '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 ─────────────────────────────────────────────────────────────────
|
-- ── contacts ─────────────────────────────────────────────────────────────────
|
||||||
DROP POLICY IF EXISTS contacts_select_members ON public.contacts;
|
CREATE POLICY "contacts_select" ON public.contacts
|
||||||
DROP POLICY IF EXISTS contacts_modify_members ON public.contacts;
|
FOR SELECT USING (space_id IN (SELECT public.get_user_space_ids(auth.uid())));
|
||||||
|
|
||||||
CREATE POLICY contacts_select_members ON public.contacts
|
CREATE POLICY "contacts_insert" ON public.contacts
|
||||||
FOR SELECT USING (space_id IN (SELECT get_user_space_ids(auth.uid())));
|
FOR INSERT WITH CHECK (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board','treasurer']));
|
||||||
|
|
||||||
CREATE POLICY contacts_modify_members ON public.contacts
|
CREATE POLICY "contacts_update" ON public.contacts
|
||||||
FOR ALL USING (space_id IN (SELECT get_user_space_ids(auth.uid())));
|
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 ────────────────────────────────────────────────────────────
|
-- ── knowledge_base ────────────────────────────────────────────────────────────
|
||||||
DROP POLICY IF EXISTS kb_select_members ON public.knowledge_base;
|
CREATE POLICY "kb_select" ON public.knowledge_base
|
||||||
DROP POLICY IF EXISTS kb_insert_members ON public.knowledge_base;
|
|
||||||
DROP POLICY IF EXISTS kb_update_members ON public.knowledge_base;
|
|
||||||
DROP POLICY IF EXISTS kb_delete_board ON public.knowledge_base;
|
|
||||||
|
|
||||||
CREATE POLICY kb_select_members ON public.knowledge_base
|
|
||||||
FOR SELECT USING (space_id IN (SELECT get_user_space_ids(auth.uid())));
|
|
||||||
|
|
||||||
CREATE POLICY kb_insert_members ON public.knowledge_base
|
|
||||||
FOR INSERT WITH CHECK (space_id IN (SELECT get_user_space_ids(auth.uid())));
|
|
||||||
|
|
||||||
CREATE POLICY kb_update_members ON public.knowledge_base
|
|
||||||
FOR UPDATE USING (space_id IN (SELECT get_user_space_ids(auth.uid())));
|
|
||||||
|
|
||||||
CREATE POLICY kb_delete_board ON public.knowledge_base
|
|
||||||
FOR DELETE USING (user_has_role_in_space(auth.uid(), space_id, ARRAY['admin', 'board']));
|
|
||||||
|
|
||||||
-- ── secrets ──────────────────────────────────────────────────────────────────
|
|
||||||
DROP POLICY IF EXISTS secrets_select_admins ON public.secrets;
|
|
||||||
DROP POLICY IF EXISTS secrets_modify_admins ON public.secrets;
|
|
||||||
|
|
||||||
-- Board/treasurer can read; only admins can write
|
|
||||||
CREATE POLICY secrets_select_admins ON public.secrets
|
|
||||||
FOR SELECT USING (
|
FOR SELECT USING (
|
||||||
user_has_role_in_space(auth.uid(), space_id, ARRAY['admin', 'board', 'treasurer'])
|
space_id IN (SELECT public.get_user_space_ids(auth.uid()))
|
||||||
|
AND (
|
||||||
|
visibility = 'all_members'
|
||||||
|
OR (visibility = 'board' AND public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board','treasurer']))
|
||||||
|
OR (visibility = 'admin_only' AND public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin']))
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE POLICY secrets_modify_admins ON public.secrets
|
CREATE POLICY "kb_insert" ON public.knowledge_base
|
||||||
FOR ALL USING (user_has_role_in_space(auth.uid(), space_id, ARRAY['admin']));
|
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']));
|
||||||
|
|
||||||
|
-- ── secrets ──────────────────────────────────────────────────────────────────
|
||||||
|
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']));
|
||||||
|
|
||||||
-- ── area_leads ───────────────────────────────────────────────────────────────
|
-- ── area_leads ───────────────────────────────────────────────────────────────
|
||||||
DROP POLICY IF EXISTS area_leads_select_members ON public.area_leads;
|
CREATE POLICY "area_leads_select" ON public.area_leads
|
||||||
DROP POLICY IF EXISTS area_leads_modify_board ON public.area_leads;
|
FOR SELECT USING (space_id IN (SELECT public.get_user_space_ids(auth.uid())));
|
||||||
|
|
||||||
CREATE POLICY area_leads_select_members ON public.area_leads
|
CREATE POLICY "area_leads_insert" ON public.area_leads
|
||||||
FOR SELECT USING (space_id IN (SELECT get_user_space_ids(auth.uid())));
|
FOR INSERT WITH CHECK (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board']));
|
||||||
|
|
||||||
CREATE POLICY area_leads_modify_board ON public.area_leads
|
CREATE POLICY "area_leads_update" ON public.area_leads
|
||||||
FOR ALL USING (user_has_role_in_space(auth.uid(), space_id, ARRAY['admin', 'board']));
|
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 ─────────────────────────────────────────────────────────────
|
-- ── integrations ─────────────────────────────────────────────────────────────
|
||||||
DROP POLICY IF EXISTS integrations_select_admins ON public.integrations;
|
CREATE POLICY "integrations_select" ON public.integrations
|
||||||
DROP POLICY IF EXISTS integrations_modify_admins ON public.integrations;
|
FOR SELECT USING (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board','treasurer']));
|
||||||
|
|
||||||
CREATE POLICY integrations_select_admins ON public.integrations
|
CREATE POLICY "integrations_insert" ON public.integrations
|
||||||
FOR SELECT USING (user_has_role_in_space(auth.uid(), space_id, ARRAY['admin']));
|
FOR INSERT WITH CHECK (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin']));
|
||||||
|
|
||||||
CREATE POLICY integrations_modify_admins ON public.integrations
|
CREATE POLICY "integrations_update" ON public.integrations
|
||||||
FOR ALL USING (user_has_role_in_space(auth.uid(), space_id, ARRAY['admin']));
|
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 ───────────────────────────────────────────────────────────
|
-- ── comms_channels ───────────────────────────────────────────────────────────
|
||||||
DROP POLICY IF EXISTS channels_select_members ON public.comms_channels;
|
CREATE POLICY "channels_select" ON public.comms_channels
|
||||||
DROP POLICY IF EXISTS channels_insert_board ON public.comms_channels;
|
FOR SELECT USING (space_id IN (SELECT public.get_user_space_ids(auth.uid())));
|
||||||
DROP POLICY IF EXISTS channels_update_board ON public.comms_channels;
|
|
||||||
DROP POLICY IF EXISTS channels_delete_board ON public.comms_channels;
|
|
||||||
|
|
||||||
CREATE POLICY channels_select_members ON public.comms_channels
|
CREATE POLICY "channels_insert" ON public.comms_channels
|
||||||
FOR SELECT USING (space_id IN (SELECT get_user_space_ids(auth.uid())));
|
FOR INSERT WITH CHECK (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board']));
|
||||||
|
|
||||||
CREATE POLICY channels_insert_board ON public.comms_channels
|
CREATE POLICY "channels_update" ON public.comms_channels
|
||||||
FOR INSERT WITH CHECK (user_has_role_in_space(auth.uid(), space_id, ARRAY['admin', 'board']));
|
FOR UPDATE USING (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board']));
|
||||||
|
|
||||||
CREATE POLICY channels_update_board ON public.comms_channels
|
CREATE POLICY "channels_delete" ON public.comms_channels
|
||||||
FOR UPDATE USING (user_has_role_in_space(auth.uid(), space_id, ARRAY['admin', 'board']));
|
FOR DELETE USING (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin']));
|
||||||
|
|
||||||
CREATE POLICY channels_delete_board ON public.comms_channels
|
|
||||||
FOR DELETE USING (user_has_role_in_space(auth.uid(), space_id, ARRAY['admin', 'board']));
|
|
||||||
|
|
||||||
-- ── comms_messages ───────────────────────────────────────────────────────────
|
-- ── comms_messages ───────────────────────────────────────────────────────────
|
||||||
DROP POLICY IF EXISTS messages_select_members ON public.comms_messages;
|
CREATE POLICY "messages_select" ON public.comms_messages
|
||||||
DROP POLICY IF EXISTS messages_insert_members ON public.comms_messages;
|
FOR SELECT USING (space_id IN (SELECT public.get_user_space_ids(auth.uid())));
|
||||||
|
|
||||||
CREATE POLICY messages_select_members ON public.comms_messages
|
CREATE POLICY "messages_insert" ON public.comms_messages
|
||||||
FOR SELECT USING (space_id IN (SELECT get_user_space_ids(auth.uid())));
|
|
||||||
|
|
||||||
-- Users may only insert messages attributed to themselves
|
|
||||||
CREATE POLICY messages_insert_members ON public.comms_messages
|
|
||||||
FOR INSERT WITH CHECK (
|
FOR INSERT WITH CHECK (
|
||||||
space_id IN (SELECT get_user_space_ids(auth.uid()))
|
space_id IN (SELECT public.get_user_space_ids(auth.uid()))
|
||||||
AND user_id = auth.uid()
|
AND user_id = auth.uid()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
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'])
|
||||||
|
);
|
||||||
|
|
||||||
-- ── activity_log ─────────────────────────────────────────────────────────────
|
-- ── activity_log ─────────────────────────────────────────────────────────────
|
||||||
DROP POLICY IF EXISTS activity_select_members ON public.activity_log;
|
CREATE POLICY "activity_select" ON public.activity_log
|
||||||
DROP POLICY IF EXISTS activity_insert_members ON public.activity_log;
|
FOR SELECT USING (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board']));
|
||||||
|
|
||||||
CREATE POLICY activity_select_members ON public.activity_log
|
CREATE POLICY "activity_insert" ON public.activity_log
|
||||||
FOR SELECT USING (space_id IN (SELECT get_user_space_ids(auth.uid())));
|
FOR INSERT WITH CHECK (space_id IN (SELECT public.get_user_space_ids(auth.uid())));
|
||||||
|
|
||||||
CREATE POLICY activity_insert_members ON public.activity_log
|
|
||||||
FOR INSERT WITH CHECK (space_id IN (SELECT get_user_space_ids(auth.uid())));
|
|
||||||
|
|
||||||
|
|
||||||
-- ─────────────────────────────────────────────────────────────────────────────
|
-- ─────────────────────────────────────────────────────────────────────────────
|
||||||
-- 7. TRIGGERS
|
-- 7. TRIGGERS & AUTH HOOK
|
||||||
-- ─────────────────────────────────────────────────────────────────────────────
|
-- ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
-- Auto-creates default channels when a new space is created
|
-- Create default comms channels when a new space is created.
|
||||||
CREATE OR REPLACE FUNCTION public.create_default_channels()
|
CREATE OR REPLACE FUNCTION public.create_default_channels()
|
||||||
RETURNS trigger
|
RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER
|
||||||
LANGUAGE plpgsql
|
|
||||||
SECURITY DEFINER
|
|
||||||
SET search_path = public
|
SET search_path = public
|
||||||
AS $$
|
AS $$
|
||||||
BEGIN
|
BEGIN
|
||||||
INSERT INTO public.comms_channels (space_id, name, channel_type) VALUES
|
INSERT INTO public.comms_channels (space_id, name, icon, channel_type)
|
||||||
(NEW.id, 'general', 'general'),
|
VALUES
|
||||||
(NEW.id, 'announcements', 'general'),
|
(NEW.id, 'general', '💬', 'general'),
|
||||||
(NEW.id, 'random', 'general'),
|
(NEW.id, 'announcements', '📢', 'general'),
|
||||||
(NEW.id, 'facilities', 'ops')
|
(NEW.id, 'ops', '⚙️', 'ops');
|
||||||
ON CONFLICT (space_id, name) DO NOTHING;
|
|
||||||
RETURN NEW;
|
RETURN NEW;
|
||||||
END;
|
END;
|
||||||
$$;
|
$$;
|
||||||
@@ -650,77 +581,43 @@ CREATE TRIGGER on_space_created
|
|||||||
AFTER INSERT ON public.spaces
|
AFTER INSERT ON public.spaces
|
||||||
FOR EACH ROW EXECUTE FUNCTION public.create_default_channels();
|
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.
|
||||||
-- 8. AUTH HOOK — fires on every new auth.users signup
|
-- NOTE: The app's signup flow calls createSpace/joinSpace server actions
|
||||||
-- ─────────────────────────────────────────────────────────────────────────────
|
-- directly after signUp() returns a session (email confirm disabled).
|
||||||
-- Reads metadata passed at signup time to either:
|
-- This trigger is a safety net for when email confirmation IS enabled.
|
||||||
-- • Create a new space and add the user as admin (space_action = 'create')
|
|
||||||
-- • Join an existing space via invite code (space_action = 'join')
|
|
||||||
--
|
|
||||||
-- Required signup metadata:
|
|
||||||
-- create: { space_action, space_name, space_slug, space_city, invite_code, full_name }
|
|
||||||
-- join: { space_action:'join', join_invite_code, full_name }
|
|
||||||
-- ─────────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
CREATE OR REPLACE FUNCTION public.handle_space_signup()
|
CREATE OR REPLACE FUNCTION public.handle_space_signup()
|
||||||
RETURNS trigger
|
RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER
|
||||||
LANGUAGE plpgsql
|
|
||||||
SECURITY DEFINER
|
|
||||||
SET search_path = public
|
SET search_path = public
|
||||||
AS $$
|
AS $$
|
||||||
DECLARE
|
DECLARE
|
||||||
v_space_id uuid;
|
v_space_id uuid;
|
||||||
v_action text;
|
v_role public.member_role;
|
||||||
v_name text;
|
v_display_name text;
|
||||||
v_slug text;
|
|
||||||
v_city text;
|
|
||||||
v_invite_code text;
|
|
||||||
BEGIN
|
BEGIN
|
||||||
v_action := COALESCE(NEW.raw_user_meta_data ->> 'space_action', '');
|
v_space_id := (NEW.raw_user_meta_data->>'space_id')::uuid;
|
||||||
|
v_role := COALESCE(
|
||||||
IF v_action = 'create' THEN
|
NULLIF((NEW.raw_user_meta_data->>'role'), '')::public.member_role,
|
||||||
v_name := NEW.raw_user_meta_data ->> 'space_name';
|
'member'
|
||||||
v_slug := NEW.raw_user_meta_data ->> 'space_slug';
|
);
|
||||||
v_city := NEW.raw_user_meta_data ->> 'space_city';
|
v_display_name := COALESCE(
|
||||||
v_invite_code := NEW.raw_user_meta_data ->> 'invite_code';
|
NULLIF(NEW.raw_user_meta_data->>'display_name', ''),
|
||||||
|
NULLIF(NEW.raw_user_meta_data->>'full_name', ''),
|
||||||
INSERT INTO public.spaces (name, slug, city, invite_code)
|
NEW.email
|
||||||
VALUES (v_name, v_slug, v_city, v_invite_code)
|
|
||||||
RETURNING id INTO v_space_id;
|
|
||||||
|
|
||||||
INSERT INTO public.space_members
|
|
||||||
(space_id, user_id, display_name, email, role, tier, status, approved)
|
|
||||||
VALUES (
|
|
||||||
v_space_id, NEW.id,
|
|
||||||
COALESCE(NEW.raw_user_meta_data ->> 'full_name', 'Admin'),
|
|
||||||
NEW.email,
|
|
||||||
'admin', 'plus', 'current', true
|
|
||||||
);
|
);
|
||||||
|
|
||||||
ELSIF v_action = 'join' THEN
|
IF v_space_id IS NULL THEN
|
||||||
v_invite_code := NEW.raw_user_meta_data ->> 'join_invite_code';
|
RETURN NEW;
|
||||||
SELECT id INTO v_space_id FROM public.spaces WHERE invite_code = v_invite_code;
|
END IF;
|
||||||
|
|
||||||
IF v_space_id IS NOT NULL THEN
|
INSERT INTO public.space_members (space_id, user_id, role, status, display_name, email, approved)
|
||||||
INSERT INTO public.space_members
|
VALUES (v_space_id, NEW.id, v_role, 'unverified', v_display_name, NEW.email, true)
|
||||||
(space_id, user_id, display_name, email, role, tier, status, approved)
|
ON CONFLICT (space_id, user_id) DO NOTHING;
|
||||||
SELECT
|
|
||||||
v_space_id, NEW.id,
|
|
||||||
COALESCE(NEW.raw_user_meta_data ->> 'full_name', 'Member'),
|
|
||||||
NEW.email,
|
|
||||||
'member', 'basic',
|
|
||||||
CASE WHEN s.require_approval THEN 'unverified' ELSE 'current' END,
|
|
||||||
CASE WHEN s.require_approval THEN false ELSE true END
|
|
||||||
FROM public.spaces s WHERE s.id = v_space_id;
|
|
||||||
END IF;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
RETURN NEW;
|
RETURN NEW;
|
||||||
END;
|
END;
|
||||||
$$;
|
$$;
|
||||||
|
|
||||||
-- Wire the auth hook (must exist in auth schema, not public)
|
|
||||||
DROP TRIGGER IF EXISTS on_auth_user_created ON auth.users;
|
DROP TRIGGER IF EXISTS on_auth_user_created ON auth.users;
|
||||||
CREATE TRIGGER on_auth_user_created
|
CREATE TRIGGER on_auth_user_created
|
||||||
AFTER INSERT ON auth.users
|
AFTER INSERT ON auth.users
|
||||||
@@ -728,6 +625,8 @@ CREATE TRIGGER on_auth_user_created
|
|||||||
|
|
||||||
|
|
||||||
-- ─────────────────────────────────────────────────────────────────────────────
|
-- ─────────────────────────────────────────────────────────────────────────────
|
||||||
-- DONE.
|
-- 8. REALTIME
|
||||||
-- For a fresh deployment, this is the only script you need to run.
|
|
||||||
-- ─────────────────────────────────────────────────────────────────────────────
|
-- ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
-- 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