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:
-14
@@ -12,20 +12,6 @@ scripts/schema.sql
|
||||
|
||||
This creates the entire schema from scratch — all tables, enums, indexes, RLS policies, functions, and triggers. Idempotent, can be run multiple times safely.
|
||||
|
||||
### Existing deployment migration
|
||||
|
||||
If you have a legacy Hackerspace database and want to sync it to the canonical schema:
|
||||
|
||||
```
|
||||
scripts/012_canonical_sync.sql
|
||||
```
|
||||
|
||||
This migration adds all missing columns, renames `full_name` → `display_name`, creates missing constraints, fixes the auth trigger, and updates all other tables to match the canonical schema. Also idempotent — safe to run multiple times.
|
||||
|
||||
### Migration history (reference only)
|
||||
|
||||
Scripts `001_*` through `011_*` represent the evolution of the live database. **Do not re-run them.** They exist for historical reference.
|
||||
|
||||
### What `schema.sql` contains
|
||||
|
||||
| Section | Contents |
|
||||
|
||||
+9
-5
@@ -13,7 +13,7 @@
|
||||
| `member_role` | `admin`, `board`, `treasurer`, `member`, `associate` |
|
||||
| `member_tier` | `plus`, `basic`, `associate` |
|
||||
| `member_status` | `current`, `late`, `inactive`, `unverified` — **NOT** `active` or `pending` |
|
||||
| `task_type` | `chore`, `task` — column is named **`type`**, NOT `task_type`. NO `ongoing` |
|
||||
| `task_type` | `chore`, `task` — DB column is **`task_type`** (enum name and column name match) |
|
||||
| `task_status` | `open`, `claimed`, `in_progress`, `overdue`, `due_today`, `completed`, `done`, `blocked` |
|
||||
| `recurrence_type` | `daily`, `weekly`, `biweekly`, `monthly`, `none` — NO `quarterly` |
|
||||
| `project_status` | `backlog`, `in_progress`, `review`, `done`, `blocked` |
|
||||
@@ -66,13 +66,14 @@
|
||||
| `payment_status` | text | nullable |
|
||||
| `payment_note` | text | nullable |
|
||||
| `joined_at` | timestamptz | |
|
||||
| `last_paid_at` | timestamptz | nullable — **NOT** `last_payment_at` |
|
||||
| `last_payment_at` | timestamptz | nullable — legacy, **also has `last_paid_at`** |
|
||||
| `last_paid_at` | timestamptz | nullable — added script 013; used by importMembers / linkPaymentToMember |
|
||||
| `has_card_access` | boolean | default false |
|
||||
| `approved` | boolean | default **true** (fixed script 008) |
|
||||
| UNIQUE | `(space_id, user_id)` | |
|
||||
| UNIQUE | `(space_id, email)` | added script 009 |
|
||||
|
||||
**Does NOT have:** `last_payment_at`, `approved_at`, `bio`, `avatar_url`
|
||||
**Does NOT have:** `last_payment_at` (has both `last_payment_at` and `last_paid_at`), `approved_at`, `bio`, `avatar_url`
|
||||
|
||||
---
|
||||
|
||||
@@ -83,7 +84,7 @@
|
||||
| `space_id` | uuid FK → spaces | NOT NULL |
|
||||
| `title` | text NOT NULL | |
|
||||
| `description` | text | nullable |
|
||||
| `type` | task_type | default `'task'` — column is **`type`**, NOT `task_type` |
|
||||
| `task_type` | task_type | default `'task'` — column is **`task_type`**, matching the enum name |
|
||||
| `status` | task_status | default `'open'` — use `'completed'` not `'done'` |
|
||||
| `area` | text | nullable |
|
||||
| `recurrence` | recurrence_type | default `'none'` |
|
||||
@@ -101,7 +102,7 @@
|
||||
| `created_at` | timestamptz | |
|
||||
| `updated_at` | timestamptz | |
|
||||
|
||||
**Does NOT have:** `task_type`, `progress`, `blocked`, `priority`
|
||||
**Does NOT have:** `type` (use `task_type`), `progress`, `blocked`, `priority`
|
||||
|
||||
---
|
||||
|
||||
@@ -386,3 +387,6 @@ Default channels created by trigger on space INSERT: `general`, `announcements`,
|
||||
| 008 | space_members.approved default=true; backfilled NULLs — fixed login loop |
|
||||
| 009 | space_members INSERT RLS: allow admin/board inserts; added UNIQUE(space_id,email) |
|
||||
| 010 | Fixed create_default_channels trigger — removed non-existent `description` column |
|
||||
| 011 | Renamed tasks `type` column to `task_type` (canonical enum column name) |
|
||||
| 012 | Canonical sync — task_type enum, cleaned duplicate columns |
|
||||
| 013 | Added `space_members.last_paid_at` timestamptz — required by importMembers and linkPaymentToMember |
|
||||
|
||||
+14
-6
@@ -59,7 +59,7 @@ export default function SignupPage() {
|
||||
setLoading(true)
|
||||
setError('')
|
||||
|
||||
// Returning user — call server actions (runs server-side, bypasses any client RLS issues)
|
||||
// Returning user — already has a session, just create/join
|
||||
if (isReturningUser) {
|
||||
const result = mode === 'create'
|
||||
? await createSpace({ spaceName, spaceSlug, spaceCity, displayName: fullName })
|
||||
@@ -70,8 +70,8 @@ export default function SignupPage() {
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
router.push('/dashboard')
|
||||
router.refresh()
|
||||
// Hard navigate so server layout re-reads cookies fresh
|
||||
window.location.href = '/dashboard'
|
||||
return
|
||||
}
|
||||
|
||||
@@ -111,7 +111,14 @@ export default function SignupPage() {
|
||||
// disabled — the user is fully signed up AND signed in in one step.
|
||||
// We still need to create the space/member record via the server action.
|
||||
if (authData.session) {
|
||||
// User is now authenticated — create the space or join via server action
|
||||
// Manually set the session so server-side cookies are flushed before
|
||||
// we call the server action (which needs auth.getUser() to succeed).
|
||||
const supabaseClient = createClient()
|
||||
await supabaseClient.auth.setSession({
|
||||
access_token: authData.session.access_token,
|
||||
refresh_token: authData.session.refresh_token,
|
||||
})
|
||||
|
||||
const result = mode === 'create'
|
||||
? await createSpace({ spaceName, spaceSlug, spaceCity, displayName: fullName })
|
||||
: await joinSpace({ inviteCode, displayName: fullName })
|
||||
@@ -121,8 +128,9 @@ export default function SignupPage() {
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
router.push('/dashboard')
|
||||
router.refresh()
|
||||
|
||||
// Hard navigate so the server re-reads cookies and picks up the session
|
||||
window.location.href = '/dashboard'
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@ export interface SpaceMember {
|
||||
export interface Task {
|
||||
id: string
|
||||
space_id: string
|
||||
type: TaskType
|
||||
task_type: TaskType
|
||||
title: string
|
||||
description?: string
|
||||
status: TaskStatus
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
-- Clean up any existing policies and tables for fresh start
|
||||
drop policy if exists "spaces_select_members" on public.spaces;
|
||||
drop policy if exists "spaces_insert_authenticated" on public.spaces;
|
||||
drop policy if exists "spaces_update_admins" on public.spaces;
|
||||
|
||||
drop policy if exists "space_members_select_own_space" on public.space_members;
|
||||
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;
|
||||
|
||||
drop table if exists public.activity_log cascade;
|
||||
drop table if exists public.comms_messages cascade;
|
||||
drop table if exists public.comms_channels cascade;
|
||||
drop table if exists public.integrations cascade;
|
||||
drop table if exists public.payments cascade;
|
||||
drop table if exists public.contacts cascade;
|
||||
drop table if exists public.area_leads cascade;
|
||||
drop table if exists public.secrets cascade;
|
||||
drop table if exists public.knowledge_base cascade;
|
||||
drop table if exists public.projects cascade;
|
||||
drop table if exists public.tasks cascade;
|
||||
drop table if exists public.space_members cascade;
|
||||
drop table if exists public.spaces cascade;
|
||||
|
||||
drop type if exists channel_type cascade;
|
||||
drop type if exists payment_link_status cascade;
|
||||
drop type if exists payment_platform cascade;
|
||||
drop type if exists contact_type cascade;
|
||||
drop type if exists area_lead_status cascade;
|
||||
drop type if exists kb_visibility cascade;
|
||||
drop type if exists project_status cascade;
|
||||
drop type if exists recurrence_type cascade;
|
||||
drop type if exists task_status cascade;
|
||||
drop type if exists task_type cascade;
|
||||
drop type if exists member_status cascade;
|
||||
drop type if exists member_tier cascade;
|
||||
drop type if exists member_role cascade;
|
||||
@@ -1,635 +0,0 @@
|
||||
-- Enable UUID extension
|
||||
create extension if not exists "uuid-ossp";
|
||||
|
||||
-- =====================================================
|
||||
-- ENUMS
|
||||
-- =====================================================
|
||||
do $$ begin
|
||||
create type member_role as enum ('admin', 'board', 'treasurer', 'member', 'associate');
|
||||
exception when duplicate_object then null; end $$;
|
||||
|
||||
do $$ begin
|
||||
create type member_tier as enum ('plus', 'basic', 'associate');
|
||||
exception when duplicate_object then null; end $$;
|
||||
|
||||
do $$ begin
|
||||
create type member_status as enum ('active', 'pending', 'inactive');
|
||||
exception when duplicate_object then null; end $$;
|
||||
|
||||
do $$ begin
|
||||
create type payment_status as enum ('current', 'late', '3mo_late', 'unverified');
|
||||
exception when duplicate_object then null; end $$;
|
||||
|
||||
do $$ begin
|
||||
create type task_type as enum ('chore', 'task');
|
||||
exception when duplicate_object then null; end $$;
|
||||
|
||||
do $$ begin
|
||||
create type task_status as enum ('open', 'claimed', 'in_progress', 'overdue', 'due_today', 'completed');
|
||||
exception when duplicate_object then null; end $$;
|
||||
|
||||
do $$ begin
|
||||
create type recurrence_type as enum ('daily', 'weekly', 'biweekly', 'monthly', 'none');
|
||||
exception when duplicate_object then null; end $$;
|
||||
|
||||
do $$ begin
|
||||
create type project_status as enum ('backlog', 'in_progress', 'review', 'done');
|
||||
exception when duplicate_object then null; end $$;
|
||||
|
||||
do $$ begin
|
||||
create type kb_visibility as enum ('all_members', 'board', 'admin_only');
|
||||
exception when duplicate_object then null; end $$;
|
||||
|
||||
do $$ begin
|
||||
create type area_lead_status as enum ('active', 'vacant', 'handoff');
|
||||
exception when duplicate_object then null; end $$;
|
||||
|
||||
do $$ begin
|
||||
create type contact_type as enum ('vendor', 'supplier', 'partner', 'landlord', 'city');
|
||||
exception when duplicate_object then null; end $$;
|
||||
|
||||
do $$ begin
|
||||
create type payment_platform as enum ('paypal', 'zeffy', 'venmo', 'cash');
|
||||
exception when duplicate_object then null; end $$;
|
||||
|
||||
do $$ begin
|
||||
create type payment_link_status as enum ('linked', 'unlinked');
|
||||
exception when duplicate_object then null; end $$;
|
||||
|
||||
do $$ begin
|
||||
create type channel_type as enum ('general', 'area', 'ops', 'project');
|
||||
exception when duplicate_object then null; end $$;
|
||||
|
||||
-- =====================================================
|
||||
-- SPACES (Multi-tenant containers)
|
||||
-- =====================================================
|
||||
create table if not exists public.spaces (
|
||||
id uuid primary key default uuid_generate_v4(),
|
||||
name text not null,
|
||||
slug text unique not null,
|
||||
city text,
|
||||
require_approval boolean default true,
|
||||
public_member_directory boolean default false,
|
||||
invite_code text unique not null,
|
||||
webhook_secret text,
|
||||
created_at timestamp with time zone default now(),
|
||||
updated_at timestamp with time zone default now()
|
||||
);
|
||||
|
||||
alter table public.spaces enable row level security;
|
||||
|
||||
-- =====================================================
|
||||
-- SPACE MEMBERS (Users in spaces with roles)
|
||||
-- =====================================================
|
||||
create table if not exists public.space_members (
|
||||
id uuid primary key default uuid_generate_v4(),
|
||||
space_id uuid references public.spaces(id) on delete cascade not null,
|
||||
user_id uuid references auth.users(id) on delete cascade,
|
||||
role text default 'member',
|
||||
tier text default 'basic',
|
||||
display_name text not null,
|
||||
email text,
|
||||
handle text,
|
||||
phone text,
|
||||
status text default 'active',
|
||||
payment_status text,
|
||||
payment_note text,
|
||||
joined_at timestamp with time zone default now(),
|
||||
last_paid_at timestamp with time zone,
|
||||
has_card_access boolean default false,
|
||||
unique(space_id, user_id)
|
||||
);
|
||||
|
||||
alter table public.space_members enable row level security;
|
||||
|
||||
-- Now add spaces RLS policies (after space_members exists)
|
||||
create policy "spaces_select_members" on public.spaces for select using (
|
||||
exists (
|
||||
select 1 from public.space_members
|
||||
where space_members.space_id = spaces.id
|
||||
and space_members.user_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
create policy "spaces_insert_authenticated" on public.spaces for insert
|
||||
with check (auth.uid() is not null);
|
||||
|
||||
create policy "spaces_update_admins" on public.spaces for update using (
|
||||
exists (
|
||||
select 1 from public.space_members
|
||||
where space_members.space_id = spaces.id
|
||||
and space_members.user_id = auth.uid()
|
||||
and space_members.role = 'admin'
|
||||
)
|
||||
);
|
||||
|
||||
-- space_members RLS policies
|
||||
create policy "space_members_select_own_space" on public.space_members for select using (
|
||||
user_id = auth.uid()
|
||||
or space_id in (
|
||||
select space_id from public.space_members where user_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
create policy "space_members_insert_authenticated" on public.space_members for insert
|
||||
with check (auth.uid() = user_id);
|
||||
|
||||
create policy "space_members_update_admins" on public.space_members for update using (
|
||||
user_id = auth.uid()
|
||||
or exists (
|
||||
select 1 from public.space_members sm
|
||||
where sm.space_id = space_members.space_id
|
||||
and sm.user_id = auth.uid()
|
||||
and sm.role = 'admin'
|
||||
)
|
||||
);
|
||||
|
||||
create policy "space_members_delete_admins" on public.space_members for delete using (
|
||||
exists (
|
||||
select 1 from public.space_members sm
|
||||
where sm.space_id = space_members.space_id
|
||||
and sm.user_id = auth.uid()
|
||||
and sm.role = 'admin'
|
||||
)
|
||||
);
|
||||
|
||||
-- =====================================================
|
||||
-- TASKS & CHORES
|
||||
-- =====================================================
|
||||
create table if not exists public.tasks (
|
||||
id uuid primary key default uuid_generate_v4(),
|
||||
space_id uuid references public.spaces(id) on delete cascade not null,
|
||||
title text not null,
|
||||
description text,
|
||||
type task_type default 'task',
|
||||
status task_status default 'open',
|
||||
area text,
|
||||
recurrence recurrence_type default 'none',
|
||||
due_date timestamp with time zone,
|
||||
assigned_to uuid references auth.users(id) on delete set null,
|
||||
assigned_to_name text,
|
||||
claimed_by uuid references auth.users(id) on delete set null,
|
||||
claimed_by_name text,
|
||||
requested_by uuid references auth.users(id) on delete set null,
|
||||
requested_by_name text,
|
||||
subtask_total integer default 0,
|
||||
subtask_completed integer default 0,
|
||||
last_done_at timestamp with time zone,
|
||||
completed_at timestamp with time zone,
|
||||
created_at timestamp with time zone default now(),
|
||||
updated_at timestamp with time zone default now()
|
||||
);
|
||||
|
||||
alter table public.tasks enable row level security;
|
||||
|
||||
create policy "tasks_select_members" on public.tasks for select using (
|
||||
exists (
|
||||
select 1 from public.space_members
|
||||
where space_members.space_id = tasks.space_id
|
||||
and space_members.user_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
create policy "tasks_insert_members" on public.tasks for insert with check (
|
||||
exists (
|
||||
select 1 from public.space_members
|
||||
where space_members.space_id = tasks.space_id
|
||||
and space_members.user_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
create policy "tasks_update_members" on public.tasks for update using (
|
||||
exists (
|
||||
select 1 from public.space_members
|
||||
where space_members.space_id = tasks.space_id
|
||||
and space_members.user_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
create policy "tasks_delete_members" on public.tasks for delete using (
|
||||
exists (
|
||||
select 1 from public.space_members
|
||||
where space_members.space_id = tasks.space_id
|
||||
and space_members.user_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
-- =====================================================
|
||||
-- PROJECTS
|
||||
-- =====================================================
|
||||
create table if not exists public.projects (
|
||||
id uuid primary key default uuid_generate_v4(),
|
||||
space_id uuid references public.spaces(id) on delete cascade not null,
|
||||
title text not null,
|
||||
description text,
|
||||
status project_status default 'backlog',
|
||||
area text,
|
||||
tags text[],
|
||||
assignee_names text[],
|
||||
task_count integer default 0,
|
||||
tasks_completed integer default 0,
|
||||
due_date timestamp with time zone,
|
||||
created_at timestamp with time zone default now(),
|
||||
updated_at timestamp with time zone default now()
|
||||
);
|
||||
|
||||
alter table public.projects enable row level security;
|
||||
|
||||
create policy "projects_select_members" on public.projects for select using (
|
||||
exists (
|
||||
select 1 from public.space_members
|
||||
where space_members.space_id = projects.space_id
|
||||
and space_members.user_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
create policy "projects_insert_members" on public.projects for insert with check (
|
||||
exists (
|
||||
select 1 from public.space_members
|
||||
where space_members.space_id = projects.space_id
|
||||
and space_members.user_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
create policy "projects_update_members" on public.projects for update using (
|
||||
exists (
|
||||
select 1 from public.space_members
|
||||
where space_members.space_id = projects.space_id
|
||||
and space_members.user_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
create policy "projects_delete_members" on public.projects for delete using (
|
||||
exists (
|
||||
select 1 from public.space_members
|
||||
where space_members.space_id = projects.space_id
|
||||
and space_members.user_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
-- =====================================================
|
||||
-- KNOWLEDGE BASE
|
||||
-- =====================================================
|
||||
create table if not exists public.knowledge_base (
|
||||
id uuid primary key default uuid_generate_v4(),
|
||||
space_id uuid references public.spaces(id) on delete cascade not null,
|
||||
title text not null,
|
||||
content text,
|
||||
icon text,
|
||||
visibility kb_visibility default 'all_members',
|
||||
area text,
|
||||
tags text[],
|
||||
is_pinned boolean default false,
|
||||
updated_by_id uuid references auth.users(id) on delete set null,
|
||||
updated_by_name text,
|
||||
created_at timestamp with time zone default now(),
|
||||
updated_at timestamp with time zone default now()
|
||||
);
|
||||
|
||||
alter table public.knowledge_base enable row level security;
|
||||
|
||||
create policy "kb_select_members" on public.knowledge_base for select using (
|
||||
exists (
|
||||
select 1 from public.space_members
|
||||
where space_members.space_id = knowledge_base.space_id
|
||||
and space_members.user_id = auth.uid()
|
||||
and (
|
||||
knowledge_base.visibility = 'all_members'
|
||||
or (knowledge_base.visibility = 'board' and space_members.role in ('admin', 'board', 'treasurer'))
|
||||
or (knowledge_base.visibility = 'admin_only' and space_members.role = 'admin')
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
create policy "kb_insert_members" on public.knowledge_base for insert with check (
|
||||
exists (
|
||||
select 1 from public.space_members
|
||||
where space_members.space_id = knowledge_base.space_id
|
||||
and space_members.user_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
create policy "kb_update_members" on public.knowledge_base for update using (
|
||||
exists (
|
||||
select 1 from public.space_members
|
||||
where space_members.space_id = knowledge_base.space_id
|
||||
and space_members.user_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
create policy "kb_delete_board" on public.knowledge_base for delete using (
|
||||
exists (
|
||||
select 1 from public.space_members
|
||||
where space_members.space_id = knowledge_base.space_id
|
||||
and space_members.user_id = auth.uid()
|
||||
and space_members.role in ('admin', 'board')
|
||||
)
|
||||
);
|
||||
|
||||
-- =====================================================
|
||||
-- SECRETS VAULT
|
||||
-- =====================================================
|
||||
create table if not exists public.secrets (
|
||||
id uuid primary key default uuid_generate_v4(),
|
||||
space_id uuid references public.spaces(id) on delete cascade not null,
|
||||
title text not null,
|
||||
icon text,
|
||||
description text,
|
||||
value text not null,
|
||||
area text,
|
||||
created_at timestamp with time zone default now(),
|
||||
updated_at timestamp with time zone default now()
|
||||
);
|
||||
|
||||
alter table public.secrets enable row level security;
|
||||
|
||||
create policy "secrets_select_admins" on public.secrets for select using (
|
||||
exists (
|
||||
select 1 from public.space_members
|
||||
where space_members.space_id = secrets.space_id
|
||||
and space_members.user_id = auth.uid()
|
||||
and space_members.role in ('admin', 'board', 'treasurer')
|
||||
)
|
||||
);
|
||||
|
||||
create policy "secrets_modify_admins" on public.secrets for all using (
|
||||
exists (
|
||||
select 1 from public.space_members
|
||||
where space_members.space_id = secrets.space_id
|
||||
and space_members.user_id = auth.uid()
|
||||
and space_members.role = 'admin'
|
||||
)
|
||||
);
|
||||
|
||||
-- =====================================================
|
||||
-- AREA LEADS
|
||||
-- =====================================================
|
||||
create table if not exists public.area_leads (
|
||||
id uuid primary key default uuid_generate_v4(),
|
||||
space_id uuid references public.spaces(id) on delete cascade not null,
|
||||
area_name text not null,
|
||||
area_code text not null,
|
||||
lead_id uuid references auth.users(id) on delete set null,
|
||||
lead_handle text,
|
||||
status area_lead_status default 'active',
|
||||
created_at timestamp with time zone default now(),
|
||||
updated_at timestamp with time zone default now()
|
||||
);
|
||||
|
||||
alter table public.area_leads enable row level security;
|
||||
|
||||
create policy "area_leads_select_members" on public.area_leads for select using (
|
||||
exists (
|
||||
select 1 from public.space_members
|
||||
where space_members.space_id = area_leads.space_id
|
||||
and space_members.user_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
create policy "area_leads_modify_board" on public.area_leads for all using (
|
||||
exists (
|
||||
select 1 from public.space_members
|
||||
where space_members.space_id = area_leads.space_id
|
||||
and space_members.user_id = auth.uid()
|
||||
and space_members.role in ('admin', 'board')
|
||||
)
|
||||
);
|
||||
|
||||
-- =====================================================
|
||||
-- CONTACTS
|
||||
-- =====================================================
|
||||
create table if not exists public.contacts (
|
||||
id uuid primary key default uuid_generate_v4(),
|
||||
space_id uuid references public.spaces(id) on delete cascade not null,
|
||||
name text not null,
|
||||
code text not null,
|
||||
contact_type contact_type not null,
|
||||
email text,
|
||||
phone text,
|
||||
details text,
|
||||
tags text[],
|
||||
created_at timestamp with time zone default now(),
|
||||
updated_at timestamp with time zone default now()
|
||||
);
|
||||
|
||||
alter table public.contacts enable row level security;
|
||||
|
||||
create policy "contacts_select_members" on public.contacts for select using (
|
||||
exists (
|
||||
select 1 from public.space_members
|
||||
where space_members.space_id = contacts.space_id
|
||||
and space_members.user_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
create policy "contacts_modify_members" on public.contacts for all using (
|
||||
exists (
|
||||
select 1 from public.space_members
|
||||
where space_members.space_id = contacts.space_id
|
||||
and space_members.user_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
-- =====================================================
|
||||
-- PAYMENTS
|
||||
-- =====================================================
|
||||
create table if not exists public.payments (
|
||||
id uuid primary key default uuid_generate_v4(),
|
||||
space_id uuid references public.spaces(id) on delete cascade not null,
|
||||
platform payment_platform not null,
|
||||
amount numeric(10,2) not null,
|
||||
from_identifier text,
|
||||
from_note text,
|
||||
member_id uuid references public.space_members(id) on delete set null,
|
||||
member_name text,
|
||||
link_status payment_link_status default 'unlinked',
|
||||
transaction_date timestamp with time zone not null,
|
||||
created_at timestamp with time zone default now()
|
||||
);
|
||||
|
||||
alter table public.payments enable row level security;
|
||||
|
||||
create policy "payments_select_treasurer" on public.payments for select using (
|
||||
exists (
|
||||
select 1 from public.space_members
|
||||
where space_members.space_id = payments.space_id
|
||||
and space_members.user_id = auth.uid()
|
||||
and space_members.role in ('admin', 'board', 'treasurer')
|
||||
)
|
||||
);
|
||||
|
||||
create policy "payments_modify_treasurer" on public.payments for all using (
|
||||
exists (
|
||||
select 1 from public.space_members
|
||||
where space_members.space_id = payments.space_id
|
||||
and space_members.user_id = auth.uid()
|
||||
and space_members.role in ('admin', 'treasurer')
|
||||
)
|
||||
);
|
||||
|
||||
-- =====================================================
|
||||
-- COMMS CHANNELS
|
||||
-- =====================================================
|
||||
create table if not exists public.comms_channels (
|
||||
id uuid primary key default uuid_generate_v4(),
|
||||
space_id uuid references public.spaces(id) on delete cascade not null,
|
||||
name text not null,
|
||||
icon text,
|
||||
channel_type channel_type default 'general',
|
||||
area_reference text,
|
||||
project_id uuid references public.projects(id) on delete cascade,
|
||||
member_count integer default 0,
|
||||
created_at timestamp with time zone default now()
|
||||
);
|
||||
|
||||
alter table public.comms_channels enable row level security;
|
||||
|
||||
create policy "channels_select_members" on public.comms_channels for select using (
|
||||
exists (
|
||||
select 1 from public.space_members
|
||||
where space_members.space_id = comms_channels.space_id
|
||||
and space_members.user_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
create policy "channels_insert_board" on public.comms_channels for insert with check (
|
||||
exists (
|
||||
select 1 from public.space_members
|
||||
where space_members.space_id = comms_channels.space_id
|
||||
and space_members.user_id = auth.uid()
|
||||
and space_members.role in ('admin', 'board')
|
||||
)
|
||||
);
|
||||
|
||||
create policy "channels_update_board" on public.comms_channels for update using (
|
||||
exists (
|
||||
select 1 from public.space_members
|
||||
where space_members.space_id = comms_channels.space_id
|
||||
and space_members.user_id = auth.uid()
|
||||
and space_members.role in ('admin', 'board')
|
||||
)
|
||||
);
|
||||
|
||||
create policy "channels_delete_board" on public.comms_channels for delete using (
|
||||
exists (
|
||||
select 1 from public.space_members
|
||||
where space_members.space_id = comms_channels.space_id
|
||||
and space_members.user_id = auth.uid()
|
||||
and space_members.role in ('admin', 'board')
|
||||
)
|
||||
);
|
||||
|
||||
-- =====================================================
|
||||
-- COMMS MESSAGES (stored for history; real-time via CLASP)
|
||||
-- =====================================================
|
||||
create table if not exists public.comms_messages (
|
||||
id uuid primary key default uuid_generate_v4(),
|
||||
channel_id uuid references public.comms_channels(id) on delete cascade not null,
|
||||
space_id uuid references public.spaces(id) on delete cascade not null,
|
||||
user_id uuid references auth.users(id) on delete set null,
|
||||
display_name text not null,
|
||||
handle text,
|
||||
content text not null,
|
||||
created_at timestamp with time zone default now()
|
||||
);
|
||||
|
||||
alter table public.comms_messages enable row level security;
|
||||
|
||||
create policy "messages_select_members" on public.comms_messages for select using (
|
||||
exists (
|
||||
select 1 from public.space_members
|
||||
where space_members.space_id = comms_messages.space_id
|
||||
and space_members.user_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
create policy "messages_insert_members" on public.comms_messages for insert with check (
|
||||
exists (
|
||||
select 1 from public.space_members
|
||||
where space_members.space_id = comms_messages.space_id
|
||||
and space_members.user_id = auth.uid()
|
||||
) and user_id = auth.uid()
|
||||
);
|
||||
|
||||
-- =====================================================
|
||||
-- INTEGRATIONS
|
||||
-- =====================================================
|
||||
create table if not exists public.integrations (
|
||||
id uuid primary key default uuid_generate_v4(),
|
||||
space_id uuid references public.spaces(id) on delete cascade not null,
|
||||
name text not null,
|
||||
platform text not null unique,
|
||||
description text,
|
||||
is_connected boolean default false,
|
||||
config jsonb,
|
||||
created_at timestamp with time zone default now(),
|
||||
updated_at timestamp with time zone default now()
|
||||
);
|
||||
|
||||
alter table public.integrations enable row level security;
|
||||
|
||||
create policy "integrations_select_admins" on public.integrations for select using (
|
||||
exists (
|
||||
select 1 from public.space_members
|
||||
where space_members.space_id = integrations.space_id
|
||||
and space_members.user_id = auth.uid()
|
||||
and space_members.role = 'admin'
|
||||
)
|
||||
);
|
||||
|
||||
create policy "integrations_modify_admins" on public.integrations for all using (
|
||||
exists (
|
||||
select 1 from public.space_members
|
||||
where space_members.space_id = integrations.space_id
|
||||
and space_members.user_id = auth.uid()
|
||||
and space_members.role = 'admin'
|
||||
)
|
||||
);
|
||||
|
||||
-- =====================================================
|
||||
-- ACTIVITY LOG
|
||||
-- =====================================================
|
||||
create table if not exists public.activity_log (
|
||||
id uuid primary key default uuid_generate_v4(),
|
||||
space_id uuid references public.spaces(id) on delete cascade not null,
|
||||
user_id uuid references auth.users(id) on delete set null,
|
||||
display_name text,
|
||||
action text not null,
|
||||
entity_type text,
|
||||
entity_id uuid,
|
||||
details text,
|
||||
created_at timestamp with time zone default now()
|
||||
);
|
||||
|
||||
alter table public.activity_log enable row level security;
|
||||
|
||||
create policy "activity_select_members" on public.activity_log for select using (
|
||||
exists (
|
||||
select 1 from public.space_members
|
||||
where space_members.space_id = activity_log.space_id
|
||||
and space_members.user_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
create policy "activity_insert_members" on public.activity_log for insert with check (
|
||||
exists (
|
||||
select 1 from public.space_members
|
||||
where space_members.space_id = activity_log.space_id
|
||||
and space_members.user_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
-- =====================================================
|
||||
-- INDEXES for Performance
|
||||
-- =====================================================
|
||||
create index if not exists idx_space_members_space_id on public.space_members(space_id);
|
||||
create index if not exists idx_space_members_user_id on public.space_members(user_id);
|
||||
create index if not exists idx_tasks_space_id on public.tasks(space_id);
|
||||
create index if not exists idx_projects_space_id on public.projects(space_id);
|
||||
create index if not exists idx_kb_space_id on public.knowledge_base(space_id);
|
||||
create index if not exists idx_messages_channel_id on public.comms_messages(channel_id);
|
||||
create index if not exists idx_messages_space_id on public.comms_messages(space_id);
|
||||
create index if not exists idx_payments_space_id on public.payments(space_id);
|
||||
create index if not exists idx_activity_space_id on public.activity_log(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);
|
||||
@@ -1,82 +0,0 @@
|
||||
-- Drop and recreate tables that need schema fixes
|
||||
-- This script fixes the schema to match the application code
|
||||
|
||||
-- Fix spaces table: rename public_directory to public_member_directory
|
||||
alter table if exists public.spaces
|
||||
rename column public_directory to public_member_directory;
|
||||
|
||||
-- Fix space_members table: use text fields instead of enums, add email/payment columns
|
||||
alter table if exists public.space_members
|
||||
add column if not exists email text,
|
||||
add column if not exists payment_status text,
|
||||
add column if not exists payment_note text,
|
||||
add column if not exists last_paid_at timestamp with time zone;
|
||||
|
||||
-- Make user_id nullable for imported members who haven't signed up yet
|
||||
alter table if exists public.space_members
|
||||
alter column user_id drop not null;
|
||||
|
||||
-- Fix tasks table: add task_type text column for easier filtering
|
||||
alter table if exists public.tasks
|
||||
add column if not exists task_type text default 'chore',
|
||||
add column if not exists progress integer default 0,
|
||||
add column if not exists recurrence text,
|
||||
add column if not exists area text,
|
||||
add column if not exists assigned_to text;
|
||||
|
||||
-- Fix projects table: add missing columns
|
||||
alter table if exists public.projects
|
||||
add column if not exists progress integer default 0,
|
||||
add column if not exists task_count integer default 0,
|
||||
add column if not exists tags text[] default '{}',
|
||||
add column if not exists category text,
|
||||
add column if not exists assignees text,
|
||||
add column if not exists due_date date,
|
||||
add column if not exists description text;
|
||||
|
||||
-- Fix knowledge_base table
|
||||
alter table if exists public.knowledge_base
|
||||
add column if not exists pinned boolean default false,
|
||||
add column if not exists area text,
|
||||
add column if not exists access_level text default 'all_members',
|
||||
add column if not exists description text,
|
||||
add column if not exists updated_by text;
|
||||
|
||||
-- Fix area_leads table
|
||||
alter table if exists public.area_leads
|
||||
add column if not exists lead_handle text,
|
||||
add column if not exists status text default 'active';
|
||||
|
||||
-- Fix contacts table
|
||||
alter table if exists public.contacts
|
||||
add column if not exists note text,
|
||||
add column if not exists group_label text;
|
||||
|
||||
-- Fix comms_channels: add member_count
|
||||
alter table if exists public.comms_channels
|
||||
add column if not exists member_count integer default 0,
|
||||
add column if not exists unread_count integer default 0;
|
||||
|
||||
-- Seed default channels when space is created via trigger
|
||||
create or replace function public.create_default_channels()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
security definer
|
||||
set search_path = public
|
||||
as $$
|
||||
begin
|
||||
insert into public.comms_channels (space_id, name, channel_type, description) values
|
||||
(new.id, 'general', 'general', 'General discussion for all members'),
|
||||
(new.id, 'announcements', 'general', 'Official announcements from board and admin'),
|
||||
(new.id, 'random', 'general', 'Off-topic chatter'),
|
||||
(new.id, 'facilities', 'ops', 'Facilities and maintenance discussion')
|
||||
on conflict do nothing;
|
||||
return new;
|
||||
end;
|
||||
$$;
|
||||
|
||||
drop trigger if exists on_space_created on public.spaces;
|
||||
create trigger on_space_created
|
||||
after insert on public.spaces
|
||||
for each row
|
||||
execute function public.create_default_channels();
|
||||
@@ -1,9 +0,0 @@
|
||||
-- Add approved column to space_members
|
||||
-- Used to distinguish members who have been approved by an admin vs pending
|
||||
alter table if exists public.space_members
|
||||
add column if not exists approved boolean default false;
|
||||
|
||||
-- Existing active members are considered approved
|
||||
update public.space_members
|
||||
set approved = true
|
||||
where status = 'active';
|
||||
@@ -1,85 +0,0 @@
|
||||
-- =====================================================
|
||||
-- This script creates a function and trigger to handle
|
||||
-- space creation/joining during signup using security definer
|
||||
-- bypassing RLS since user hasn't confirmed email yet
|
||||
-- =====================================================
|
||||
|
||||
-- Function to handle creating a space and member during signup
|
||||
create or replace function public.handle_space_signup()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
security definer
|
||||
set search_path = public
|
||||
as $$
|
||||
declare
|
||||
v_space_id uuid;
|
||||
v_action text;
|
||||
v_space_name text;
|
||||
v_space_slug text;
|
||||
v_space_city text;
|
||||
v_invite_code text;
|
||||
begin
|
||||
-- Get metadata from user signup
|
||||
v_action := coalesce(new.raw_user_meta_data ->> 'space_action', '');
|
||||
|
||||
if v_action = 'create' then
|
||||
v_space_name := new.raw_user_meta_data ->> 'space_name';
|
||||
v_space_slug := new.raw_user_meta_data ->> 'space_slug';
|
||||
v_space_city := new.raw_user_meta_data ->> 'space_city';
|
||||
v_invite_code := new.raw_user_meta_data ->> 'invite_code';
|
||||
|
||||
-- Create the space
|
||||
insert into public.spaces (name, slug, city, invite_code)
|
||||
values (v_space_name, v_space_slug, v_space_city, v_invite_code)
|
||||
returning id into v_space_id;
|
||||
|
||||
-- Add user as admin
|
||||
insert into public.space_members (space_id, user_id, display_name, email, role, tier, status)
|
||||
values (
|
||||
v_space_id,
|
||||
new.id,
|
||||
coalesce(new.raw_user_meta_data ->> 'full_name', 'Admin'),
|
||||
new.email,
|
||||
'admin',
|
||||
'plus',
|
||||
'active'
|
||||
);
|
||||
|
||||
-- Create default channels
|
||||
insert into public.comms_channels (space_id, name, channel_type) values
|
||||
(v_space_id, 'general', 'general'),
|
||||
(v_space_id, 'announcements', 'general'),
|
||||
(v_space_id, 'random', 'general');
|
||||
|
||||
elsif v_action = 'join' then
|
||||
v_invite_code := new.raw_user_meta_data ->> 'join_invite_code';
|
||||
|
||||
-- Find space by invite code
|
||||
select id into v_space_id from public.spaces where invite_code = v_invite_code;
|
||||
|
||||
if v_space_id is not null then
|
||||
-- Add user as member (pending if require_approval is true)
|
||||
insert into public.space_members (space_id, user_id, display_name, email, role, tier, status)
|
||||
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 'pending' else 'active' end
|
||||
from public.spaces s where s.id = v_space_id;
|
||||
end if;
|
||||
end if;
|
||||
|
||||
return new;
|
||||
end;
|
||||
$$;
|
||||
|
||||
-- Create trigger
|
||||
drop trigger if exists on_auth_user_space_signup on auth.users;
|
||||
|
||||
create trigger on_auth_user_space_signup
|
||||
after insert on auth.users
|
||||
for each row
|
||||
execute function public.handle_space_signup();
|
||||
@@ -1,39 +0,0 @@
|
||||
-- Fix infinite recursion in space_members RLS policy
|
||||
-- The original policy referenced space_members while evaluating a policy on space_members
|
||||
|
||||
-- Drop the problematic policy
|
||||
drop policy if exists "space_members_select_own_space" on public.space_members;
|
||||
|
||||
-- Create a simpler policy that avoids self-reference
|
||||
-- Users can see their own record, plus all members in any space they belong to
|
||||
-- We use a subquery on a different approach to avoid recursion
|
||||
|
||||
create policy "space_members_select_own" on public.space_members for select using (
|
||||
-- User can always see their own membership record
|
||||
user_id = auth.uid()
|
||||
);
|
||||
|
||||
-- Create a separate policy for viewing other members in the same space
|
||||
-- This uses a function to break the recursion cycle
|
||||
create or replace function public.get_user_space_ids(uid uuid)
|
||||
returns setof uuid
|
||||
language sql
|
||||
security definer
|
||||
stable
|
||||
as $$
|
||||
select space_id from public.space_members where user_id = uid
|
||||
$$;
|
||||
|
||||
create policy "space_members_select_same_space" on public.space_members for select using (
|
||||
space_id in (select public.get_user_space_ids(auth.uid()))
|
||||
);
|
||||
|
||||
-- Also fix the insert policy to allow users to insert themselves into a space
|
||||
-- when joining via invite code (they won't have a record yet)
|
||||
drop policy if exists "space_members_insert_authenticated" on public.space_members;
|
||||
|
||||
create policy "space_members_insert_authenticated" on public.space_members for insert
|
||||
with check (
|
||||
auth.uid() is not null
|
||||
and user_id = auth.uid()
|
||||
);
|
||||
@@ -1,20 +0,0 @@
|
||||
-- Fix spaces INSERT policy to allow any authenticated user to create a space
|
||||
-- This is needed for the signup flow when a user creates their first space
|
||||
|
||||
-- Drop existing insert policy if it exists
|
||||
drop policy if exists "spaces_insert_authenticated" on public.spaces;
|
||||
|
||||
-- Create a simple policy allowing any authenticated user to insert
|
||||
create policy "spaces_insert_authenticated" on public.spaces
|
||||
for insert
|
||||
with check (auth.uid() is not null);
|
||||
|
||||
-- Also fix space_members INSERT policy to allow users to add themselves
|
||||
drop policy if exists "space_members_insert_authenticated" on public.space_members;
|
||||
|
||||
create policy "space_members_insert_authenticated" on public.space_members
|
||||
for insert
|
||||
with check (
|
||||
auth.uid() is not null
|
||||
and user_id = auth.uid()
|
||||
);
|
||||
@@ -1,516 +0,0 @@
|
||||
-- =====================================================
|
||||
-- COMPREHENSIVE RLS FIX
|
||||
-- Fixes infinite recursion + enum cast issues
|
||||
-- =====================================================
|
||||
|
||||
-- Helper: returns the space_ids the user belongs to (security definer avoids recursion)
|
||||
create or replace function public.get_user_space_ids(uid uuid)
|
||||
returns setof uuid
|
||||
language sql
|
||||
security definer
|
||||
stable
|
||||
set search_path = public
|
||||
as $$
|
||||
select space_id from public.space_members where user_id = uid
|
||||
$$;
|
||||
|
||||
-- Helper: checks if user has one of the given roles in a space
|
||||
-- NOTE: role column is a member_role enum, so we cast to text for comparison
|
||||
create or replace function public.user_has_role_in_space(uid uuid, sid uuid, allowed_roles text[])
|
||||
returns boolean
|
||||
language sql
|
||||
security definer
|
||||
stable
|
||||
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)
|
||||
)
|
||||
$$;
|
||||
|
||||
-- Grant execute permissions
|
||||
grant execute on function public.get_user_space_ids(uuid) to authenticated, service_role;
|
||||
grant execute on function public.user_has_role_in_space(uuid, uuid, text[]) to authenticated, service_role;
|
||||
|
||||
-- =====================================================
|
||||
-- SPACES
|
||||
-- =====================================================
|
||||
drop policy if exists "spaces_select_members" on public.spaces;
|
||||
drop policy if exists "spaces_insert_authenticated" on public.spaces;
|
||||
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
|
||||
for select using (id in (select public.get_user_space_ids(auth.uid())));
|
||||
|
||||
create policy "spaces_insert_authenticated" on public.spaces
|
||||
for insert with check (auth.uid() is not null);
|
||||
|
||||
create policy "spaces_update_admins" on public.spaces
|
||||
for update using (public.user_has_role_in_space(auth.uid(), id, array['admin']));
|
||||
|
||||
create policy "spaces_delete_admins" on public.spaces
|
||||
for delete using (public.user_has_role_in_space(auth.uid(), id, array['admin']));
|
||||
|
||||
-- =====================================================
|
||||
-- SPACE MEMBERS
|
||||
-- =====================================================
|
||||
drop policy if exists "space_members_select_own" on public.space_members;
|
||||
drop policy if exists "space_members_select_same_space" on public.space_members;
|
||||
drop policy if exists "space_members_select_own_space" on public.space_members;
|
||||
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;
|
||||
|
||||
create policy "space_members_select_own" on public.space_members
|
||||
for select using (user_id = auth.uid());
|
||||
|
||||
create policy "space_members_select_same_space" on public.space_members
|
||||
for select using (space_id in (select public.get_user_space_ids(auth.uid())));
|
||||
|
||||
-- Any authenticated user can insert a row where user_id = themselves
|
||||
create policy "space_members_insert_authenticated" on public.space_members
|
||||
for insert with check (auth.uid() is not null and user_id = auth.uid());
|
||||
|
||||
-- Users can update their own profile; admins can update anyone in their space
|
||||
create policy "space_members_update_admins" on public.space_members
|
||||
for update using (
|
||||
user_id = auth.uid()
|
||||
or public.user_has_role_in_space(auth.uid(), space_id, array['admin'])
|
||||
);
|
||||
|
||||
create policy "space_members_delete_admins" on public.space_members
|
||||
for delete using (
|
||||
public.user_has_role_in_space(auth.uid(), space_id, array['admin'])
|
||||
);
|
||||
|
||||
-- =====================================================
|
||||
-- TASKS
|
||||
-- =====================================================
|
||||
drop policy if exists "tasks_select_members" on public.tasks;
|
||||
drop policy if exists "tasks_insert_members" on public.tasks;
|
||||
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
|
||||
for select using (space_id in (select public.get_user_space_ids(auth.uid())));
|
||||
|
||||
create policy "tasks_insert_members" on public.tasks
|
||||
for insert with check (space_id in (select public.get_user_space_ids(auth.uid())));
|
||||
|
||||
create policy "tasks_update_members" on public.tasks
|
||||
for update using (space_id in (select public.get_user_space_ids(auth.uid())));
|
||||
|
||||
create policy "tasks_delete_members" on public.tasks
|
||||
for delete using (space_id in (select public.get_user_space_ids(auth.uid())));
|
||||
|
||||
-- =====================================================
|
||||
-- PROJECTS
|
||||
-- =====================================================
|
||||
drop policy if exists "projects_select_members" on public.projects;
|
||||
drop policy if exists "projects_insert_members" on public.projects;
|
||||
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
|
||||
for select using (space_id in (select public.get_user_space_ids(auth.uid())));
|
||||
|
||||
create policy "projects_insert_members" on public.projects
|
||||
for insert with check (space_id in (select public.get_user_space_ids(auth.uid())));
|
||||
|
||||
create policy "projects_update_members" on public.projects
|
||||
for update using (space_id in (select public.get_user_space_ids(auth.uid())));
|
||||
|
||||
create policy "projects_delete_members" on public.projects
|
||||
for delete using (space_id in (select public.get_user_space_ids(auth.uid())));
|
||||
|
||||
-- =====================================================
|
||||
-- KNOWLEDGE BASE
|
||||
-- =====================================================
|
||||
drop policy if exists "kb_select_members" 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 public.get_user_space_ids(auth.uid())));
|
||||
|
||||
create policy "kb_insert_members" on public.knowledge_base
|
||||
for insert with check (space_id in (select public.get_user_space_ids(auth.uid())));
|
||||
|
||||
create policy "kb_update_members" on public.knowledge_base
|
||||
for update using (space_id in (select public.get_user_space_ids(auth.uid())));
|
||||
|
||||
create policy "kb_delete_board" on public.knowledge_base
|
||||
for delete using (public.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;
|
||||
|
||||
create policy "secrets_select_admins" on public.secrets
|
||||
for select using (
|
||||
public.user_has_role_in_space(auth.uid(), space_id, array['admin', 'board', 'treasurer'])
|
||||
);
|
||||
|
||||
create policy "secrets_modify_admins" on public.secrets
|
||||
for all using (public.user_has_role_in_space(auth.uid(), space_id, array['admin']));
|
||||
|
||||
-- =====================================================
|
||||
-- AREA LEADS
|
||||
-- =====================================================
|
||||
drop policy if exists "area_leads_select_members" on public.area_leads;
|
||||
drop policy if exists "area_leads_modify_board" on public.area_leads;
|
||||
|
||||
create policy "area_leads_select_members" on public.area_leads
|
||||
for select using (space_id in (select public.get_user_space_ids(auth.uid())));
|
||||
|
||||
create policy "area_leads_modify_board" on public.area_leads
|
||||
for all using (public.user_has_role_in_space(auth.uid(), space_id, array['admin', 'board']));
|
||||
|
||||
-- =====================================================
|
||||
-- CONTACTS
|
||||
-- =====================================================
|
||||
drop policy if exists "contacts_select_members" on public.contacts;
|
||||
drop policy if exists "contacts_modify_members" on public.contacts;
|
||||
|
||||
create policy "contacts_select_members" on public.contacts
|
||||
for select using (space_id in (select public.get_user_space_ids(auth.uid())));
|
||||
|
||||
create policy "contacts_modify_members" on public.contacts
|
||||
for all using (space_id in (select public.get_user_space_ids(auth.uid())));
|
||||
|
||||
-- =====================================================
|
||||
-- PAYMENTS
|
||||
-- =====================================================
|
||||
drop policy if exists "payments_select_treasurer" on public.payments;
|
||||
drop policy if exists "payments_modify_treasurer" on public.payments;
|
||||
|
||||
create policy "payments_select_treasurer" on public.payments
|
||||
for select using (
|
||||
public.user_has_role_in_space(auth.uid(), space_id, array['admin', 'board', 'treasurer'])
|
||||
);
|
||||
|
||||
create policy "payments_modify_treasurer" on public.payments
|
||||
for all using (
|
||||
public.user_has_role_in_space(auth.uid(), space_id, array['admin', 'treasurer'])
|
||||
);
|
||||
|
||||
-- =====================================================
|
||||
-- COMMS CHANNELS
|
||||
-- =====================================================
|
||||
drop policy if exists "channels_select_members" on public.comms_channels;
|
||||
drop policy if exists "channels_insert_board" on public.comms_channels;
|
||||
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
|
||||
for select using (space_id in (select public.get_user_space_ids(auth.uid())));
|
||||
|
||||
create policy "channels_insert_board" 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_board" on public.comms_channels
|
||||
for update using (public.user_has_role_in_space(auth.uid(), space_id, array['admin', 'board']));
|
||||
|
||||
create policy "channels_delete_board" on public.comms_channels
|
||||
for delete using (public.user_has_role_in_space(auth.uid(), space_id, array['admin', 'board']));
|
||||
|
||||
-- =====================================================
|
||||
-- COMMS MESSAGES
|
||||
-- =====================================================
|
||||
drop policy if exists "messages_select_members" on public.comms_messages;
|
||||
drop policy if exists "messages_insert_members" on public.comms_messages;
|
||||
|
||||
create policy "messages_select_members" on public.comms_messages
|
||||
for select using (space_id in (select public.get_user_space_ids(auth.uid())));
|
||||
|
||||
create policy "messages_insert_members" on public.comms_messages
|
||||
for insert with check (
|
||||
space_id in (select public.get_user_space_ids(auth.uid()))
|
||||
and user_id = auth.uid()
|
||||
);
|
||||
|
||||
-- =====================================================
|
||||
-- INTEGRATIONS
|
||||
-- =====================================================
|
||||
drop policy if exists "integrations_select_admins" on public.integrations;
|
||||
drop policy if exists "integrations_modify_admins" on public.integrations;
|
||||
|
||||
create policy "integrations_select_admins" on public.integrations
|
||||
for select using (public.user_has_role_in_space(auth.uid(), space_id, array['admin']));
|
||||
|
||||
create policy "integrations_modify_admins" on public.integrations
|
||||
for all using (public.user_has_role_in_space(auth.uid(), space_id, array['admin']));
|
||||
|
||||
-- =====================================================
|
||||
-- ACTIVITY LOG
|
||||
-- =====================================================
|
||||
drop policy if exists "activity_select_members" on public.activity_log;
|
||||
drop policy if exists "activity_insert_members" on public.activity_log;
|
||||
|
||||
create policy "activity_select_members" on public.activity_log
|
||||
for select using (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 public.get_user_space_ids(auth.uid())));
|
||||
|
||||
|
||||
-- =====================================================
|
||||
-- SPACES
|
||||
-- =====================================================
|
||||
drop policy if exists "spaces_select_members" on public.spaces;
|
||||
drop policy if exists "spaces_insert_authenticated" on public.spaces;
|
||||
drop policy if exists "spaces_update_admins" on public.spaces;
|
||||
|
||||
create policy "spaces_select_members" on public.spaces for select using (
|
||||
id in (select public.get_user_space_ids(auth.uid()))
|
||||
);
|
||||
|
||||
create policy "spaces_insert_authenticated" on public.spaces for insert
|
||||
with check (auth.uid() is not null);
|
||||
|
||||
create policy "spaces_update_admins" on public.spaces for update using (
|
||||
public.user_has_role_in_space(auth.uid(), id, array['admin'])
|
||||
);
|
||||
|
||||
-- =====================================================
|
||||
-- SPACE MEMBERS
|
||||
-- =====================================================
|
||||
drop policy if exists "space_members_select_own" on public.space_members;
|
||||
drop policy if exists "space_members_select_same_space" on public.space_members;
|
||||
drop policy if exists "space_members_select_own_space" on public.space_members;
|
||||
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;
|
||||
|
||||
-- SELECT: Users can see their own record
|
||||
create policy "space_members_select_own" on public.space_members for select using (
|
||||
user_id = auth.uid()
|
||||
);
|
||||
|
||||
-- SELECT: Users can see all members in spaces they belong to
|
||||
create policy "space_members_select_same_space" on public.space_members for select using (
|
||||
space_id in (select public.get_user_space_ids(auth.uid()))
|
||||
);
|
||||
|
||||
-- INSERT: Authenticated users can add themselves to a space
|
||||
create policy "space_members_insert_authenticated" on public.space_members for insert
|
||||
with check (auth.uid() is not null and user_id = auth.uid());
|
||||
|
||||
-- UPDATE: Admins can update any member, users can update themselves
|
||||
create policy "space_members_update_admins" on public.space_members for update using (
|
||||
user_id = auth.uid()
|
||||
or public.user_has_role_in_space(auth.uid(), space_id, array['admin'])
|
||||
);
|
||||
|
||||
-- DELETE: Only admins can remove members
|
||||
create policy "space_members_delete_admins" on public.space_members for delete using (
|
||||
public.user_has_role_in_space(auth.uid(), space_id, array['admin'])
|
||||
);
|
||||
|
||||
-- =====================================================
|
||||
-- TASKS
|
||||
-- =====================================================
|
||||
drop policy if exists "tasks_select_members" on public.tasks;
|
||||
drop policy if exists "tasks_insert_members" on public.tasks;
|
||||
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 for select using (
|
||||
space_id in (select public.get_user_space_ids(auth.uid()))
|
||||
);
|
||||
|
||||
create policy "tasks_insert_members" on public.tasks for insert with check (
|
||||
space_id in (select public.get_user_space_ids(auth.uid()))
|
||||
);
|
||||
|
||||
create policy "tasks_update_members" on public.tasks for update using (
|
||||
space_id in (select public.get_user_space_ids(auth.uid()))
|
||||
);
|
||||
|
||||
create policy "tasks_delete_members" on public.tasks for delete using (
|
||||
space_id in (select public.get_user_space_ids(auth.uid()))
|
||||
);
|
||||
|
||||
-- =====================================================
|
||||
-- PROJECTS
|
||||
-- =====================================================
|
||||
drop policy if exists "projects_select_members" on public.projects;
|
||||
drop policy if exists "projects_insert_members" on public.projects;
|
||||
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 for select using (
|
||||
space_id in (select public.get_user_space_ids(auth.uid()))
|
||||
);
|
||||
|
||||
create policy "projects_insert_members" on public.projects for insert with check (
|
||||
space_id in (select public.get_user_space_ids(auth.uid()))
|
||||
);
|
||||
|
||||
create policy "projects_update_members" on public.projects for update using (
|
||||
space_id in (select public.get_user_space_ids(auth.uid()))
|
||||
);
|
||||
|
||||
create policy "projects_delete_members" on public.projects for delete using (
|
||||
space_id in (select public.get_user_space_ids(auth.uid()))
|
||||
);
|
||||
|
||||
-- =====================================================
|
||||
-- KNOWLEDGE BASE
|
||||
-- =====================================================
|
||||
drop policy if exists "kb_select_members" 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 public.get_user_space_ids(auth.uid()))
|
||||
);
|
||||
|
||||
create policy "kb_insert_members" on public.knowledge_base for insert with check (
|
||||
space_id in (select public.get_user_space_ids(auth.uid()))
|
||||
);
|
||||
|
||||
create policy "kb_update_members" on public.knowledge_base for update using (
|
||||
space_id in (select public.get_user_space_ids(auth.uid()))
|
||||
);
|
||||
|
||||
create policy "kb_delete_board" on public.knowledge_base for delete using (
|
||||
public.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;
|
||||
|
||||
create policy "secrets_select_admins" on public.secrets for select using (
|
||||
public.user_has_role_in_space(auth.uid(), space_id, array['admin', 'board', 'treasurer'])
|
||||
);
|
||||
|
||||
create policy "secrets_modify_admins" on public.secrets for all using (
|
||||
public.user_has_role_in_space(auth.uid(), space_id, array['admin'])
|
||||
);
|
||||
|
||||
-- =====================================================
|
||||
-- AREA LEADS
|
||||
-- =====================================================
|
||||
drop policy if exists "area_leads_select_members" on public.area_leads;
|
||||
drop policy if exists "area_leads_modify_board" on public.area_leads;
|
||||
|
||||
create policy "area_leads_select_members" on public.area_leads for select using (
|
||||
space_id in (select public.get_user_space_ids(auth.uid()))
|
||||
);
|
||||
|
||||
create policy "area_leads_modify_board" on public.area_leads for all using (
|
||||
public.user_has_role_in_space(auth.uid(), space_id, array['admin', 'board'])
|
||||
);
|
||||
|
||||
-- =====================================================
|
||||
-- CONTACTS
|
||||
-- =====================================================
|
||||
drop policy if exists "contacts_select_members" on public.contacts;
|
||||
drop policy if exists "contacts_modify_members" on public.contacts;
|
||||
|
||||
create policy "contacts_select_members" on public.contacts for select using (
|
||||
space_id in (select public.get_user_space_ids(auth.uid()))
|
||||
);
|
||||
|
||||
create policy "contacts_modify_members" on public.contacts for all using (
|
||||
space_id in (select public.get_user_space_ids(auth.uid()))
|
||||
);
|
||||
|
||||
-- =====================================================
|
||||
-- PAYMENTS
|
||||
-- =====================================================
|
||||
drop policy if exists "payments_select_treasurer" on public.payments;
|
||||
drop policy if exists "payments_modify_treasurer" on public.payments;
|
||||
|
||||
create policy "payments_select_treasurer" on public.payments for select using (
|
||||
public.user_has_role_in_space(auth.uid(), space_id, array['admin', 'board', 'treasurer'])
|
||||
);
|
||||
|
||||
create policy "payments_modify_treasurer" on public.payments for all using (
|
||||
public.user_has_role_in_space(auth.uid(), space_id, array['admin', 'treasurer'])
|
||||
);
|
||||
|
||||
-- =====================================================
|
||||
-- COMMS CHANNELS
|
||||
-- =====================================================
|
||||
drop policy if exists "channels_select_members" on public.comms_channels;
|
||||
drop policy if exists "channels_insert_board" on public.comms_channels;
|
||||
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 for select using (
|
||||
space_id in (select public.get_user_space_ids(auth.uid()))
|
||||
);
|
||||
|
||||
create policy "channels_insert_board" 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_board" on public.comms_channels for update using (
|
||||
public.user_has_role_in_space(auth.uid(), space_id, array['admin', 'board'])
|
||||
);
|
||||
|
||||
create policy "channels_delete_board" on public.comms_channels for delete using (
|
||||
public.user_has_role_in_space(auth.uid(), space_id, array['admin', 'board'])
|
||||
);
|
||||
|
||||
-- =====================================================
|
||||
-- COMMS MESSAGES
|
||||
-- =====================================================
|
||||
drop policy if exists "messages_select_members" on public.comms_messages;
|
||||
drop policy if exists "messages_insert_members" on public.comms_messages;
|
||||
|
||||
create policy "messages_select_members" on public.comms_messages for select using (
|
||||
space_id in (select public.get_user_space_ids(auth.uid()))
|
||||
);
|
||||
|
||||
create policy "messages_insert_members" on public.comms_messages for insert with check (
|
||||
space_id in (select public.get_user_space_ids(auth.uid()))
|
||||
and user_id = auth.uid()
|
||||
);
|
||||
|
||||
-- =====================================================
|
||||
-- INTEGRATIONS
|
||||
-- =====================================================
|
||||
drop policy if exists "integrations_select_admins" on public.integrations;
|
||||
drop policy if exists "integrations_modify_admins" on public.integrations;
|
||||
|
||||
create policy "integrations_select_admins" on public.integrations for select using (
|
||||
public.user_has_role_in_space(auth.uid(), space_id, array['admin'])
|
||||
);
|
||||
|
||||
create policy "integrations_modify_admins" on public.integrations for all using (
|
||||
public.user_has_role_in_space(auth.uid(), space_id, array['admin'])
|
||||
);
|
||||
|
||||
-- =====================================================
|
||||
-- ACTIVITY LOG
|
||||
-- =====================================================
|
||||
drop policy if exists "activity_select_members" on public.activity_log;
|
||||
drop policy if exists "activity_insert_members" on public.activity_log;
|
||||
|
||||
create policy "activity_select_members" on public.activity_log for select using (
|
||||
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 public.get_user_space_ids(auth.uid()))
|
||||
);
|
||||
|
||||
-- =====================================================
|
||||
-- GRANT EXECUTE on helper functions to authenticated users
|
||||
-- =====================================================
|
||||
grant execute on function public.get_user_space_ids(uuid) to authenticated;
|
||||
grant execute on function public.user_has_role_in_space(uuid, uuid, text[]) to authenticated;
|
||||
@@ -1,62 +0,0 @@
|
||||
-- =====================================================
|
||||
-- 007: Fix schema + RLS issues found in full audit
|
||||
-- =====================================================
|
||||
|
||||
-- 1. Fix task_status enum — add 'done' and 'completed' alias
|
||||
-- The code uses 'done' but enum only has 'completed'
|
||||
-- Safest: add 'done' to the enum
|
||||
ALTER TYPE task_status ADD VALUE IF NOT EXISTS 'done';
|
||||
ALTER TYPE task_status ADD VALUE IF NOT EXISTS 'blocked';
|
||||
|
||||
-- 2. Fix project_status enum — add 'blocked' for dashboard display
|
||||
ALTER TYPE project_status ADD VALUE IF NOT EXISTS 'blocked';
|
||||
|
||||
-- 3. Add missing columns to contacts
|
||||
ALTER TABLE public.contacts
|
||||
ADD COLUMN IF NOT EXISTS note text,
|
||||
ADD COLUMN IF NOT EXISTS group_label text;
|
||||
|
||||
-- 4. Fix integrations unique constraint
|
||||
-- Current: platform is globally unique (wrong — each space should have its own)
|
||||
-- Drop the global unique and add a composite unique
|
||||
ALTER TABLE public.integrations DROP CONSTRAINT IF EXISTS integrations_platform_key;
|
||||
ALTER TABLE public.integrations
|
||||
ADD CONSTRAINT integrations_space_platform_unique UNIQUE (space_id, platform);
|
||||
|
||||
-- 5. Fix area_leads unique constraint (needed for upsert onConflict)
|
||||
ALTER TABLE public.area_leads
|
||||
ADD CONSTRAINT area_leads_space_area_unique UNIQUE (space_id, area_code);
|
||||
|
||||
-- 6. Fix space_members_insert_authenticated — allow admins to add members without user accounts
|
||||
-- (null user_id is valid for manually-added members)
|
||||
DROP POLICY IF EXISTS "space_members_insert_authenticated" ON public.space_members;
|
||||
CREATE POLICY "space_members_insert_authenticated" ON public.space_members
|
||||
FOR INSERT WITH CHECK (
|
||||
-- User can add themselves
|
||||
(auth.uid() IS NOT NULL AND user_id = auth.uid())
|
||||
OR
|
||||
-- Admin can add a member record (possibly without user_id) to their own space
|
||||
(
|
||||
auth.uid() IS NOT NULL AND (user_id IS NULL OR user_id != auth.uid()) AND
|
||||
EXISTS (
|
||||
SELECT 1 FROM public.space_members sm
|
||||
WHERE sm.user_id = auth.uid()
|
||||
AND sm.space_id = space_members.space_id
|
||||
AND sm.role::text IN ('admin', 'board')
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
-- 7. Make sure activity_log allows service_role inserts (for triggers)
|
||||
GRANT ALL ON public.activity_log TO service_role;
|
||||
GRANT ALL ON public.space_members TO service_role;
|
||||
GRANT ALL ON public.spaces TO service_role;
|
||||
|
||||
-- 8. Add progress column to projects if missing
|
||||
ALTER TABLE public.projects
|
||||
ADD COLUMN IF NOT EXISTS progress integer DEFAULT 0;
|
||||
|
||||
-- 9. Add name alias for projects (code uses project.name but column is title)
|
||||
-- Add a generated column as alias
|
||||
ALTER TABLE public.projects
|
||||
ADD COLUMN IF NOT EXISTS name text GENERATED ALWAYS AS (title) STORED;
|
||||
@@ -1,12 +0,0 @@
|
||||
-- 008: Set approved default to true so new members don't get locked out
|
||||
-- The approved column was added without a default, meaning it defaults to NULL.
|
||||
-- Queries filtering eq('approved', true) would silently return zero rows.
|
||||
-- Fix: set the default to true so any insert that doesn't specify it gets approved automatically.
|
||||
|
||||
ALTER TABLE public.space_members
|
||||
ALTER COLUMN approved SET DEFAULT true;
|
||||
|
||||
-- Also backfill existing members who have null approved
|
||||
UPDATE public.space_members
|
||||
SET approved = true
|
||||
WHERE approved IS NULL;
|
||||
@@ -1,32 +0,0 @@
|
||||
-- Fix 1: Allow admins to insert space_members records without requiring user_id = auth.uid()
|
||||
-- This is needed for: addMember (offline/manual member add), importMembers (CSV bulk import)
|
||||
-- The current policy only allows self-registration (user_id = auth.uid())
|
||||
-- We need to also allow board/admin members of the same space to insert
|
||||
|
||||
-- Drop the restrictive insert policy
|
||||
DROP POLICY IF EXISTS space_members_insert_authenticated ON public.space_members;
|
||||
|
||||
-- New insert policy: allow self-registration OR admin/board inserting into their own space
|
||||
-- Cast role and status to text to avoid enum comparison errors
|
||||
CREATE POLICY space_members_insert_authenticated ON public.space_members
|
||||
FOR INSERT
|
||||
TO authenticated
|
||||
WITH CHECK (
|
||||
-- Self registration: user is inserting their own record
|
||||
user_id = auth.uid()
|
||||
OR
|
||||
-- Admin/board adding a member to their space (offline member, no user_id yet)
|
||||
public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin', 'board'])
|
||||
);
|
||||
|
||||
-- Fix 2: Add unique constraint on space_members(space_id, email) for importMembers upsert
|
||||
-- Only add if not already present
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint
|
||||
WHERE conname = 'space_members_space_id_email_key'
|
||||
) THEN
|
||||
ALTER TABLE public.space_members ADD CONSTRAINT space_members_space_id_email_key UNIQUE (space_id, email);
|
||||
END IF;
|
||||
END $$;
|
||||
@@ -1,27 +0,0 @@
|
||||
-- Fix the create_default_channels trigger to not use description column
|
||||
-- The comms_channels table does NOT have a description column
|
||||
|
||||
create or replace function public.create_default_channels()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
security definer
|
||||
set search_path = public
|
||||
as $$
|
||||
begin
|
||||
-- Insert default channels without description (column doesn't exist)
|
||||
insert into public.comms_channels (space_id, name, channel_type) values
|
||||
(new.id, 'general', 'general'),
|
||||
(new.id, 'announcements', 'general'),
|
||||
(new.id, 'random', 'general'),
|
||||
(new.id, 'facilities', 'ops')
|
||||
on conflict do nothing;
|
||||
return new;
|
||||
end;
|
||||
$$;
|
||||
|
||||
-- Recreate the trigger
|
||||
drop trigger if exists on_space_created on public.spaces;
|
||||
create trigger on_space_created
|
||||
after insert on public.spaces
|
||||
for each row
|
||||
execute function public.create_default_channels();
|
||||
@@ -1,93 +0,0 @@
|
||||
-- =====================================================
|
||||
-- 011: FIX ALL ENUM MISMATCHES
|
||||
-- Ground truth from live DB:
|
||||
-- member_status = current, late, inactive, unverified
|
||||
-- (NO 'active', NO 'pending')
|
||||
--
|
||||
-- mapping:
|
||||
-- 'active' -> 'current'
|
||||
-- 'pending' -> 'unverified' (awaiting approval)
|
||||
-- 'inactive'-> 'inactive' (same)
|
||||
-- =====================================================
|
||||
|
||||
-- 1. Fix handle_space_signup trigger to use correct enum values
|
||||
CREATE OR REPLACE FUNCTION public.handle_space_signup()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = 'public'
|
||||
AS $$
|
||||
DECLARE
|
||||
v_space_id uuid;
|
||||
v_action text;
|
||||
v_space_name text;
|
||||
v_space_slug text;
|
||||
v_space_city text;
|
||||
v_invite_code text;
|
||||
BEGIN
|
||||
v_action := coalesce(new.raw_user_meta_data ->> 'space_action', '');
|
||||
|
||||
IF v_action = 'create' THEN
|
||||
v_space_name := new.raw_user_meta_data ->> 'space_name';
|
||||
v_space_slug := new.raw_user_meta_data ->> 'space_slug';
|
||||
v_space_city := new.raw_user_meta_data ->> 'space_city';
|
||||
v_invite_code := new.raw_user_meta_data ->> 'invite_code';
|
||||
|
||||
INSERT INTO public.spaces (name, slug, city, invite_code)
|
||||
VALUES (v_space_name, v_space_slug, v_space_city, v_invite_code)
|
||||
RETURNING id INTO v_space_id;
|
||||
|
||||
-- 'current' is the correct active member_status value
|
||||
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
|
||||
v_invite_code := new.raw_user_meta_data ->> 'join_invite_code';
|
||||
|
||||
SELECT id INTO v_space_id FROM public.spaces WHERE invite_code = v_invite_code;
|
||||
|
||||
IF v_space_id IS NOT NULL THEN
|
||||
INSERT INTO public.space_members (space_id, user_id, display_name, email, role, tier, status, approved)
|
||||
SELECT
|
||||
v_space_id,
|
||||
new.id,
|
||||
coalesce(new.raw_user_meta_data ->> 'full_name', 'Member'),
|
||||
new.email,
|
||||
'member',
|
||||
'basic',
|
||||
-- 'unverified' = awaiting approval, 'current' = directly active
|
||||
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;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- 2. Fix the RLS helper to not filter on status at all (any member of the space, regardless of status)
|
||||
-- This ensures even unverified members can at least log in and see their space
|
||||
CREATE OR REPLACE FUNCTION public.get_user_space_ids(uid uuid)
|
||||
RETURNS SETOF uuid
|
||||
LANGUAGE sql
|
||||
SECURITY DEFINER
|
||||
STABLE
|
||||
SET search_path = 'public'
|
||||
AS $$
|
||||
SELECT space_id FROM public.space_members WHERE user_id = uid
|
||||
$$;
|
||||
|
||||
-- 3. Backfill any existing 'active' or 'pending' values that are stuck
|
||||
-- These were inserted by old buggy code
|
||||
UPDATE public.space_members SET status = 'current'::member_status
|
||||
WHERE status::text = 'active';
|
||||
@@ -1,193 +0,0 @@
|
||||
-- Migration 012: Sync existing databases to canonical schema
|
||||
-- Run this on any existing Hackerspace deployment to align with the canonical schema.sql
|
||||
-- Safe to run multiple times (idempotent).
|
||||
--
|
||||
-- Changes:
|
||||
-- • Rename space_members.full_name → display_name (to match app code everywhere)
|
||||
-- • Drop redundant tasks.type text column (canonical column is task_type enum)
|
||||
-- • Add missing columns to all tables that the app code expects
|
||||
-- • Create missing unique constraints for upserts
|
||||
-- • Update handle_space_signup trigger to match canonical version
|
||||
|
||||
-- ============================================================
|
||||
-- RENAME COLUMNS
|
||||
-- ============================================================
|
||||
ALTER TABLE public.space_members RENAME COLUMN full_name TO display_name;
|
||||
ALTER TABLE public.space_members ADD COLUMN IF NOT EXISTS handle text;
|
||||
ALTER TABLE public.space_members ADD COLUMN IF NOT EXISTS bio text;
|
||||
ALTER TABLE public.space_members ADD COLUMN IF NOT EXISTS avatar_url text;
|
||||
ALTER TABLE public.space_members ADD COLUMN IF NOT EXISTS dues_paid_until timestamptz;
|
||||
ALTER TABLE public.space_members ADD COLUMN IF NOT EXISTS stripe_customer_id text;
|
||||
ALTER TABLE public.space_members ADD COLUMN IF NOT EXISTS has_card_access boolean NOT NULL DEFAULT false;
|
||||
ALTER TABLE public.space_members ADD COLUMN IF NOT EXISTS payment_status text;
|
||||
ALTER TABLE public.space_members ADD COLUMN IF NOT EXISTS payment_note text;
|
||||
ALTER TABLE public.space_members ADD COLUMN IF NOT EXISTS last_payment_at timestamptz;
|
||||
ALTER TABLE public.space_members ADD COLUMN IF NOT EXISTS updated_at timestamptz NOT NULL DEFAULT now();
|
||||
|
||||
-- ============================================================
|
||||
-- TASKS TABLE FIXES
|
||||
-- ============================================================
|
||||
-- Drop the redundant text 'type' column if it exists (canonical column is task_type enum)
|
||||
ALTER TABLE public.tasks DROP COLUMN IF EXISTS type CASCADE;
|
||||
|
||||
-- Ensure all canonical task columns exist
|
||||
ALTER TABLE public.tasks ADD COLUMN IF NOT EXISTS priority text NOT NULL DEFAULT 'medium';
|
||||
ALTER TABLE public.tasks ADD COLUMN IF NOT EXISTS requested_by uuid REFERENCES auth.users(id) ON DELETE SET NULL;
|
||||
ALTER TABLE public.tasks ADD COLUMN IF NOT EXISTS requested_by_name text;
|
||||
ALTER TABLE public.tasks ADD COLUMN IF NOT EXISTS claimed_by_name text;
|
||||
ALTER TABLE public.tasks ADD COLUMN IF NOT EXISTS assigned_to_name text;
|
||||
ALTER TABLE public.tasks ADD COLUMN IF NOT EXISTS completed_at timestamptz;
|
||||
ALTER TABLE public.tasks ADD COLUMN IF NOT EXISTS last_done_at timestamptz;
|
||||
ALTER TABLE public.tasks ADD COLUMN IF NOT EXISTS subtask_total integer NOT NULL DEFAULT 0;
|
||||
ALTER TABLE public.tasks ADD COLUMN IF NOT EXISTS subtask_completed integer NOT NULL DEFAULT 0;
|
||||
ALTER TABLE public.tasks ADD COLUMN IF NOT EXISTS progress integer NOT NULL DEFAULT 0;
|
||||
|
||||
-- ============================================================
|
||||
-- SPACES TABLE
|
||||
-- ============================================================
|
||||
ALTER TABLE public.spaces ADD COLUMN IF NOT EXISTS city text;
|
||||
ALTER TABLE public.spaces ADD COLUMN IF NOT EXISTS address text;
|
||||
ALTER TABLE public.spaces ADD COLUMN IF NOT EXISTS timezone text DEFAULT 'America/Phoenix';
|
||||
ALTER TABLE public.spaces ADD COLUMN IF NOT EXISTS invite_code text;
|
||||
ALTER TABLE public.spaces ADD COLUMN IF NOT EXISTS require_approval boolean NOT NULL DEFAULT true;
|
||||
ALTER TABLE public.spaces ADD COLUMN IF NOT EXISTS public_member_directory boolean NOT NULL DEFAULT false;
|
||||
ALTER TABLE public.spaces ADD COLUMN IF NOT EXISTS webhook_secret text;
|
||||
|
||||
-- ============================================================
|
||||
-- PAYMENTS TABLE
|
||||
-- ============================================================
|
||||
ALTER TABLE public.payments ADD COLUMN IF NOT EXISTS from_identifier text;
|
||||
ALTER TABLE public.payments ADD COLUMN IF NOT EXISTS from_note text;
|
||||
ALTER TABLE public.payments ADD COLUMN IF NOT EXISTS member_name text;
|
||||
ALTER TABLE public.payments ADD COLUMN IF NOT EXISTS link_status public.payment_link_status NOT NULL DEFAULT 'unlinked';
|
||||
ALTER TABLE public.payments ADD COLUMN IF NOT EXISTS transaction_date timestamptz DEFAULT now();
|
||||
|
||||
-- ============================================================
|
||||
-- CONTACTS TABLE
|
||||
-- ============================================================
|
||||
ALTER TABLE public.contacts ADD COLUMN IF NOT EXISTS code text;
|
||||
ALTER TABLE public.contacts ADD COLUMN IF NOT EXISTS details text;
|
||||
ALTER TABLE public.contacts ADD COLUMN IF NOT EXISTS note text;
|
||||
ALTER TABLE public.contacts ADD COLUMN IF NOT EXISTS group_label text;
|
||||
|
||||
-- ============================================================
|
||||
-- KNOWLEDGE BASE
|
||||
-- ============================================================
|
||||
ALTER TABLE public.knowledge_base ADD COLUMN IF NOT EXISTS icon text;
|
||||
ALTER TABLE public.knowledge_base ADD COLUMN IF NOT EXISTS access_level text DEFAULT 'all_members';
|
||||
ALTER TABLE public.knowledge_base ADD COLUMN IF NOT EXISTS area text;
|
||||
ALTER TABLE public.knowledge_base ADD COLUMN IF NOT EXISTS is_pinned boolean NOT NULL DEFAULT false;
|
||||
ALTER TABLE public.knowledge_base ADD COLUMN IF NOT EXISTS updated_by_id uuid REFERENCES public.space_members(id) ON DELETE SET NULL;
|
||||
ALTER TABLE public.knowledge_base ADD COLUMN IF NOT EXISTS updated_by_name text;
|
||||
ALTER TABLE public.knowledge_base ADD COLUMN IF NOT EXISTS updated_by text;
|
||||
|
||||
-- ============================================================
|
||||
-- SECRETS TABLE
|
||||
-- ============================================================
|
||||
ALTER TABLE public.secrets ADD COLUMN IF NOT EXISTS icon text;
|
||||
ALTER TABLE public.secrets ADD COLUMN IF NOT EXISTS description text;
|
||||
ALTER TABLE public.secrets ADD COLUMN IF NOT EXISTS area text;
|
||||
ALTER TABLE public.secrets ADD COLUMN IF NOT EXISTS title text;
|
||||
-- Backfill title from label where label exists and title is null
|
||||
UPDATE public.secrets SET title = label WHERE title IS NULL AND label IS NOT NULL;
|
||||
|
||||
-- ============================================================
|
||||
-- AREA LEADS TABLE
|
||||
-- ============================================================
|
||||
ALTER TABLE public.area_leads ADD COLUMN IF NOT EXISTS area_code text;
|
||||
ALTER TABLE public.area_leads ADD COLUMN IF NOT EXISTS lead_handle text;
|
||||
|
||||
-- Add unique constraint on (space_id, area_code) if not already present
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint
|
||||
WHERE conname = 'area_leads_space_area_code_key'
|
||||
) THEN
|
||||
ALTER TABLE public.area_leads ADD CONSTRAINT area_leads_space_area_code_key
|
||||
UNIQUE (space_id, area_code) DEFERRABLE INITIALLY DEFERRED;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- ============================================================
|
||||
-- INTEGRATIONS TABLE
|
||||
-- ============================================================
|
||||
ALTER TABLE public.integrations ADD COLUMN IF NOT EXISTS name text;
|
||||
ALTER TABLE public.integrations ADD COLUMN IF NOT EXISTS description text;
|
||||
ALTER TABLE public.integrations ADD COLUMN IF NOT EXISTS config jsonb;
|
||||
|
||||
-- ============================================================
|
||||
-- COMMS CHANNELS
|
||||
-- ============================================================
|
||||
ALTER TABLE public.comms_channels ADD COLUMN IF NOT EXISTS icon text;
|
||||
ALTER TABLE public.comms_channels ADD COLUMN IF NOT EXISTS area_reference text;
|
||||
ALTER TABLE public.comms_channels ADD COLUMN IF NOT EXISTS member_count integer NOT NULL DEFAULT 0;
|
||||
ALTER TABLE public.comms_channels ADD COLUMN IF NOT EXISTS unread_count integer NOT NULL DEFAULT 0;
|
||||
|
||||
-- ============================================================
|
||||
-- COMMS MESSAGES
|
||||
-- ============================================================
|
||||
ALTER TABLE public.comms_messages ADD COLUMN IF NOT EXISTS display_name text;
|
||||
ALTER TABLE public.comms_messages ADD COLUMN IF NOT EXISTS handle text;
|
||||
|
||||
-- ============================================================
|
||||
-- ACTIVITY LOG
|
||||
-- ============================================================
|
||||
ALTER TABLE public.activity_log ADD COLUMN IF NOT EXISTS display_name text;
|
||||
ALTER TABLE public.activity_log ADD COLUMN IF NOT EXISTS details text;
|
||||
|
||||
-- ============================================================
|
||||
-- PROJECTS TABLE
|
||||
-- ============================================================
|
||||
ALTER TABLE public.projects ADD COLUMN IF NOT EXISTS name text;
|
||||
ALTER TABLE public.projects ADD COLUMN IF NOT EXISTS area text;
|
||||
ALTER TABLE public.projects ADD COLUMN IF NOT EXISTS category text;
|
||||
ALTER TABLE public.projects ADD COLUMN IF NOT EXISTS assignees text;
|
||||
ALTER TABLE public.projects ADD COLUMN IF NOT EXISTS assignee_names text[];
|
||||
ALTER TABLE public.projects ADD COLUMN IF NOT EXISTS task_count integer NOT NULL DEFAULT 0;
|
||||
ALTER TABLE public.projects ADD COLUMN IF NOT EXISTS tasks_completed integer NOT NULL DEFAULT 0;
|
||||
ALTER TABLE public.projects ADD COLUMN IF NOT EXISTS progress integer NOT NULL DEFAULT 0;
|
||||
|
||||
-- ============================================================
|
||||
-- UNIQUE INDEXES / CONSTRAINTS
|
||||
-- ============================================================
|
||||
-- space_members: unique index on (space_id, email) for upsert in importMembers
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_space_members_space_email
|
||||
ON public.space_members (space_id, email)
|
||||
WHERE email IS NOT NULL;
|
||||
|
||||
-- ============================================================
|
||||
-- UPDATE TRIGGERS
|
||||
-- ============================================================
|
||||
-- Fix handle_space_signup to match canonical version
|
||||
CREATE OR REPLACE FUNCTION public.handle_space_signup()
|
||||
RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER
|
||||
SET search_path = public
|
||||
AS $$
|
||||
DECLARE
|
||||
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
|
||||
);
|
||||
|
||||
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)
|
||||
ON CONFLICT (space_id, user_id) DO NOTHING;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
+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