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 <1184415+virgilvox@users.noreply.github.com>
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.
|
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
|
### What `schema.sql` contains
|
||||||
|
|
||||||
| Section | Contents |
|
| Section | Contents |
|
||||||
|
|||||||
+9
-5
@@ -13,7 +13,7 @@
|
|||||||
| `member_role` | `admin`, `board`, `treasurer`, `member`, `associate` |
|
| `member_role` | `admin`, `board`, `treasurer`, `member`, `associate` |
|
||||||
| `member_tier` | `plus`, `basic`, `associate` |
|
| `member_tier` | `plus`, `basic`, `associate` |
|
||||||
| `member_status` | `current`, `late`, `inactive`, `unverified` — **NOT** `active` or `pending` |
|
| `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` |
|
| `task_status` | `open`, `claimed`, `in_progress`, `overdue`, `due_today`, `completed`, `done`, `blocked` |
|
||||||
| `recurrence_type` | `daily`, `weekly`, `biweekly`, `monthly`, `none` — NO `quarterly` |
|
| `recurrence_type` | `daily`, `weekly`, `biweekly`, `monthly`, `none` — NO `quarterly` |
|
||||||
| `project_status` | `backlog`, `in_progress`, `review`, `done`, `blocked` |
|
| `project_status` | `backlog`, `in_progress`, `review`, `done`, `blocked` |
|
||||||
@@ -66,13 +66,14 @@
|
|||||||
| `payment_status` | text | nullable |
|
| `payment_status` | text | nullable |
|
||||||
| `payment_note` | text | nullable |
|
| `payment_note` | text | nullable |
|
||||||
| `joined_at` | timestamptz | |
|
| `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 |
|
| `has_card_access` | boolean | default false |
|
||||||
| `approved` | boolean | default **true** (fixed script 008) |
|
| `approved` | boolean | default **true** (fixed script 008) |
|
||||||
| UNIQUE | `(space_id, user_id)` | |
|
| UNIQUE | `(space_id, user_id)` | |
|
||||||
| UNIQUE | `(space_id, email)` | added script 009 |
|
| 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 |
|
| `space_id` | uuid FK → spaces | NOT NULL |
|
||||||
| `title` | text NOT NULL | |
|
| `title` | text NOT NULL | |
|
||||||
| `description` | text | nullable |
|
| `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'` |
|
| `status` | task_status | default `'open'` — use `'completed'` not `'done'` |
|
||||||
| `area` | text | nullable |
|
| `area` | text | nullable |
|
||||||
| `recurrence` | recurrence_type | default `'none'` |
|
| `recurrence` | recurrence_type | default `'none'` |
|
||||||
@@ -101,7 +102,7 @@
|
|||||||
| `created_at` | timestamptz | |
|
| `created_at` | timestamptz | |
|
||||||
| `updated_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 |
|
| 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) |
|
| 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 |
|
| 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)
|
setLoading(true)
|
||||||
setError('')
|
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) {
|
if (isReturningUser) {
|
||||||
const result = mode === 'create'
|
const result = mode === 'create'
|
||||||
? await createSpace({ spaceName, spaceSlug, spaceCity, displayName: fullName })
|
? await createSpace({ spaceName, spaceSlug, spaceCity, displayName: fullName })
|
||||||
@@ -70,8 +70,8 @@ export default function SignupPage() {
|
|||||||
setLoading(false)
|
setLoading(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
router.push('/dashboard')
|
// Hard navigate so server layout re-reads cookies fresh
|
||||||
router.refresh()
|
window.location.href = '/dashboard'
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,7 +111,14 @@ export default function SignupPage() {
|
|||||||
// disabled — the user is fully signed up AND signed in in one step.
|
// 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.
|
// We still need to create the space/member record via the server action.
|
||||||
if (authData.session) {
|
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'
|
const result = mode === 'create'
|
||||||
? await createSpace({ spaceName, spaceSlug, spaceCity, displayName: fullName })
|
? await createSpace({ spaceName, spaceSlug, spaceCity, displayName: fullName })
|
||||||
: await joinSpace({ inviteCode, displayName: fullName })
|
: await joinSpace({ inviteCode, displayName: fullName })
|
||||||
@@ -121,8 +128,9 @@ export default function SignupPage() {
|
|||||||
setLoading(false)
|
setLoading(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
router.push('/dashboard')
|
|
||||||
router.refresh()
|
// Hard navigate so the server re-reads cookies and picks up the session
|
||||||
|
window.location.href = '/dashboard'
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -49,7 +49,7 @@ export interface SpaceMember {
|
|||||||
export interface Task {
|
export interface Task {
|
||||||
id: string
|
id: string
|
||||||
space_id: string
|
space_id: string
|
||||||
type: TaskType
|
task_type: TaskType
|
||||||
title: string
|
title: string
|
||||||
description?: string
|
description?: string
|
||||||
status: TaskStatus
|
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.
|
-- Single file. Run top-to-bottom in Supabase SQL Editor on a fresh project.
|
||||||
-- It is fully idempotent: safe to run again if interrupted.
|
-- Idempotent: uses IF NOT EXISTS / OR REPLACE throughout.
|
||||||
|
-- No supplemental scripts required.
|
||||||
--
|
--
|
||||||
-- Execution order:
|
-- Sections:
|
||||||
-- 1. Extensions
|
-- 1. Extensions
|
||||||
-- 2. Enums
|
-- 2. Enums
|
||||||
-- 3. Tables (spaces → space_members → everything else)
|
-- 3. Tables (spaces → space_members → rest)
|
||||||
-- 4. Unique constraints & indexes
|
-- 4. Indexes
|
||||||
-- 5. Helper functions (reference space_members → must come after tables)
|
-- 5. Helper functions (must come after tables)
|
||||||
-- 6. Row Level Security
|
-- 6. Row Level Security
|
||||||
-- 7. Triggers & auth hook
|
-- 7. Triggers & auth hook
|
||||||
-- 8. Realtime
|
-- 8. Realtime
|
||||||
-- =============================================================================
|
-- =============================================================================
|
||||||
|
|
||||||
-- ─────────────────────────────────────────────────────────────────────────────
|
-- -----------------------------------------------------------------------------
|
||||||
-- 1. EXTENSIONS
|
-- 1. EXTENSIONS
|
||||||
-- ─────────────────────────────────────────────────────────────────────────────
|
-- -----------------------------------------------------------------------------
|
||||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||||
|
|
||||||
|
|
||||||
-- ─────────────────────────────────────────────────────────────────────────────
|
-- -----------------------------------------------------------------------------
|
||||||
-- 2. ENUMS
|
-- 2. ENUMS
|
||||||
-- ─────────────────────────────────────────────────────────────────────────────
|
-- -----------------------------------------------------------------------------
|
||||||
DO $$ BEGIN CREATE TYPE public.member_role AS ENUM ('admin','board','treasurer','member','associate'); EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
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_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.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_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.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.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.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_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.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.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.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.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.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 (
|
CREATE TABLE IF NOT EXISTS public.spaces (
|
||||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
name text NOT NULL,
|
name text NOT NULL,
|
||||||
@@ -52,24 +53,21 @@ CREATE TABLE IF NOT EXISTS public.spaces (
|
|||||||
logo_url text,
|
logo_url text,
|
||||||
city text,
|
city text,
|
||||||
address text,
|
address text,
|
||||||
timezone text NOT NULL DEFAULT 'America/Phoenix',
|
timezone text DEFAULT 'America/Phoenix',
|
||||||
invite_code text,
|
invite_code text,
|
||||||
require_approval boolean NOT NULL DEFAULT true,
|
require_approval boolean DEFAULT true,
|
||||||
public_member_directory boolean NOT NULL DEFAULT false,
|
public_member_directory boolean DEFAULT false,
|
||||||
webhook_secret text,
|
webhook_secret text,
|
||||||
settings jsonb NOT NULL DEFAULT '{}',
|
settings jsonb DEFAULT '{}',
|
||||||
created_at timestamptz NOT NULL DEFAULT now(),
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
updated_at timestamptz NOT NULL DEFAULT now()
|
updated_at timestamptz NOT NULL DEFAULT now()
|
||||||
);
|
);
|
||||||
|
|
||||||
-- ── space_members ─────────────────────────────────────────────────────────────
|
-- space_members ---------------------------------------------------------------
|
||||||
-- One row per (user × space).
|
|
||||||
-- display_name — the member's chosen name within this space
|
|
||||||
-- handle — optional @handle (unique within a space is enforced by app, not DB)
|
|
||||||
CREATE TABLE IF NOT EXISTS public.space_members (
|
CREATE TABLE IF NOT EXISTS public.space_members (
|
||||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
space_id uuid NOT NULL REFERENCES public.spaces(id) ON DELETE CASCADE,
|
space_id uuid NOT NULL REFERENCES public.spaces(id) ON DELETE CASCADE,
|
||||||
user_id uuid 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',
|
role public.member_role NOT NULL DEFAULT 'member',
|
||||||
tier public.member_tier NOT NULL DEFAULT 'basic',
|
tier public.member_tier NOT NULL DEFAULT 'basic',
|
||||||
status public.member_status NOT NULL DEFAULT 'unverified',
|
status public.member_status NOT NULL DEFAULT 'unverified',
|
||||||
@@ -83,8 +81,9 @@ CREATE TABLE IF NOT EXISTS public.space_members (
|
|||||||
has_card_access boolean NOT NULL DEFAULT false,
|
has_card_access boolean NOT NULL DEFAULT false,
|
||||||
payment_status text,
|
payment_status text,
|
||||||
payment_note text,
|
payment_note text,
|
||||||
|
last_payment_at timestamptz,
|
||||||
last_paid_at timestamptz,
|
last_paid_at timestamptz,
|
||||||
joined_at timestamptz NOT NULL DEFAULT now(),
|
joined_at timestamptz DEFAULT now(),
|
||||||
dues_paid_until timestamptz,
|
dues_paid_until timestamptz,
|
||||||
stripe_customer_id text,
|
stripe_customer_id text,
|
||||||
created_at timestamptz NOT NULL DEFAULT now(),
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
@@ -92,44 +91,55 @@ CREATE TABLE IF NOT EXISTS public.space_members (
|
|||||||
UNIQUE (space_id, user_id)
|
UNIQUE (space_id, user_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
-- ── projects ─────────────────────────────────────────────────────────────────
|
-- Partial unique index: enforce uniqueness on email only when present
|
||||||
-- Must be created before tasks (tasks.project_id FK).
|
-- 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 (
|
CREATE TABLE IF NOT EXISTS public.projects (
|
||||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
space_id uuid NOT NULL REFERENCES public.spaces(id) ON DELETE CASCADE,
|
space_id uuid NOT NULL REFERENCES public.spaces(id) ON DELETE CASCADE,
|
||||||
title text NOT NULL,
|
title text NOT NULL,
|
||||||
|
name text,
|
||||||
description text,
|
description text,
|
||||||
status public.project_status NOT NULL DEFAULT 'backlog',
|
|
||||||
area text,
|
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,
|
due_date timestamptz,
|
||||||
task_count integer NOT NULL DEFAULT 0,
|
tags text[],
|
||||||
tasks_completed integer NOT NULL DEFAULT 0,
|
task_count integer NOT NULL DEFAULT 0,
|
||||||
progress integer NOT NULL DEFAULT 0,
|
tasks_completed integer NOT NULL DEFAULT 0,
|
||||||
created_at timestamptz NOT NULL DEFAULT now(),
|
progress integer NOT NULL DEFAULT 0,
|
||||||
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()
|
||||||
);
|
);
|
||||||
|
|
||||||
-- ── tasks ────────────────────────────────────────────────────────────────────
|
-- tasks -----------------------------------------------------------------------
|
||||||
-- task_type — the enum column (canonical)
|
|
||||||
-- status — task lifecycle; both 'completed' and 'done' mean finished
|
|
||||||
CREATE TABLE IF NOT EXISTS public.tasks (
|
CREATE TABLE IF NOT EXISTS public.tasks (
|
||||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
space_id uuid NOT NULL REFERENCES public.spaces(id) ON DELETE CASCADE,
|
space_id uuid NOT NULL REFERENCES public.spaces(id) ON DELETE CASCADE,
|
||||||
title text NOT NULL,
|
title text NOT NULL,
|
||||||
description text,
|
description text,
|
||||||
task_type public.task_type NOT NULL DEFAULT 'task',
|
task_type public.task_type NOT NULL DEFAULT 'task',
|
||||||
status public.task_status NOT NULL DEFAULT 'open',
|
status public.task_status NOT NULL DEFAULT 'open',
|
||||||
recurrence public.recurrence_type NOT NULL DEFAULT 'none',
|
recurrence public.recurrence_type DEFAULT 'none',
|
||||||
priority text NOT NULL DEFAULT 'medium',
|
priority text DEFAULT 'medium',
|
||||||
area text,
|
area text,
|
||||||
project_id uuid REFERENCES public.projects(id) ON DELETE SET NULL,
|
|
||||||
tags text[],
|
tags text[],
|
||||||
due_date timestamptz,
|
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 uuid REFERENCES auth.users(id) ON DELETE SET NULL,
|
||||||
claimed_by_name text,
|
claimed_by_name text,
|
||||||
assigned_to uuid REFERENCES auth.users(id) ON DELETE SET NULL,
|
assigned_to uuid REFERENCES auth.users(id) ON DELETE SET NULL,
|
||||||
assigned_to_name text,
|
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 uuid REFERENCES auth.users(id) ON DELETE SET NULL,
|
||||||
requested_by_name text,
|
requested_by_name text,
|
||||||
subtask_total integer NOT NULL DEFAULT 0,
|
subtask_total integer NOT NULL DEFAULT 0,
|
||||||
@@ -137,144 +147,167 @@ CREATE TABLE IF NOT EXISTS public.tasks (
|
|||||||
progress integer NOT NULL DEFAULT 0,
|
progress integer NOT NULL DEFAULT 0,
|
||||||
last_done_at timestamptz,
|
last_done_at timestamptz,
|
||||||
completed_at timestamptz,
|
completed_at timestamptz,
|
||||||
created_by uuid REFERENCES auth.users(id) ON DELETE SET NULL,
|
|
||||||
created_at timestamptz NOT NULL DEFAULT now(),
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
updated_at timestamptz NOT NULL DEFAULT now()
|
updated_at timestamptz NOT NULL DEFAULT now()
|
||||||
);
|
);
|
||||||
|
|
||||||
-- ── payments ─────────────────────────────────────────────────────────────────
|
-- payments --------------------------------------------------------------------
|
||||||
-- from_identifier — email / @handle / name from source platform
|
|
||||||
-- from_note — memo / note from sender
|
|
||||||
-- link_status — whether this payment has been matched to a member
|
|
||||||
CREATE TABLE IF NOT EXISTS public.payments (
|
CREATE TABLE IF NOT EXISTS public.payments (
|
||||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
space_id uuid NOT NULL REFERENCES public.spaces(id) ON DELETE CASCADE,
|
space_id uuid NOT NULL REFERENCES public.spaces(id) ON DELETE CASCADE,
|
||||||
|
member_id uuid REFERENCES public.space_members(id) ON DELETE SET NULL,
|
||||||
platform public.payment_platform NOT NULL,
|
platform public.payment_platform NOT NULL,
|
||||||
amount numeric(10,2) 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_identifier text,
|
||||||
from_note text,
|
from_note text,
|
||||||
member_id uuid REFERENCES public.space_members(id) ON DELETE SET NULL,
|
|
||||||
member_name text,
|
member_name text,
|
||||||
link_status public.payment_link_status NOT NULL DEFAULT 'unlinked',
|
payer_name text,
|
||||||
transaction_date timestamptz NOT NULL DEFAULT now(),
|
payer_email text,
|
||||||
created_at timestamptz NOT NULL DEFAULT now()
|
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 ─────────────────────────────────────────────────────────────────
|
-- contacts --------------------------------------------------------------------
|
||||||
-- code — short auto-generated identifier (e.g. "VEN234")
|
|
||||||
CREATE TABLE IF NOT EXISTS public.contacts (
|
CREATE TABLE IF NOT EXISTS public.contacts (
|
||||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
space_id uuid NOT NULL REFERENCES public.spaces(id) ON DELETE CASCADE,
|
space_id uuid NOT NULL REFERENCES public.spaces(id) ON DELETE CASCADE,
|
||||||
name text NOT NULL,
|
name text NOT NULL,
|
||||||
code text NOT NULL,
|
contact_type public.contact_type NOT NULL DEFAULT 'vendor',
|
||||||
contact_type public.contact_type NOT NULL DEFAULT 'vendor',
|
code text,
|
||||||
email text,
|
email text,
|
||||||
phone text,
|
phone text,
|
||||||
|
address text,
|
||||||
|
notes text,
|
||||||
details text,
|
details text,
|
||||||
note text,
|
note text,
|
||||||
group_label text,
|
group_label text,
|
||||||
tags text[],
|
tags text[],
|
||||||
created_at timestamptz NOT NULL DEFAULT now(),
|
created_by uuid REFERENCES auth.users(id) ON DELETE SET NULL,
|
||||||
updated_at timestamptz NOT NULL DEFAULT now()
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now()
|
||||||
);
|
);
|
||||||
|
|
||||||
-- ── knowledge_base ────────────────────────────────────────────────────────────
|
-- knowledge_base --------------------------------------------------------------
|
||||||
-- is_pinned — canonical pinned flag
|
|
||||||
-- updated_by_id — FK to space_members (for display in UI)
|
|
||||||
-- updated_by_name — denormalized display name for performance
|
|
||||||
CREATE TABLE IF NOT EXISTS public.knowledge_base (
|
CREATE TABLE IF NOT EXISTS public.knowledge_base (
|
||||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
space_id uuid NOT NULL REFERENCES public.spaces(id) ON DELETE CASCADE,
|
space_id uuid NOT NULL REFERENCES public.spaces(id) ON DELETE CASCADE,
|
||||||
title text NOT NULL,
|
title text NOT NULL,
|
||||||
description text,
|
|
||||||
content text,
|
content text,
|
||||||
icon text,
|
category text,
|
||||||
visibility public.kb_visibility NOT NULL DEFAULT 'all_members',
|
|
||||||
area text,
|
area text,
|
||||||
|
icon text,
|
||||||
tags 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,
|
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_id uuid REFERENCES public.space_members(id) ON DELETE SET NULL,
|
||||||
updated_by_name text,
|
updated_by_name text,
|
||||||
created_at timestamptz NOT NULL DEFAULT now(),
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
updated_at timestamptz NOT NULL DEFAULT now()
|
updated_at timestamptz NOT NULL DEFAULT now()
|
||||||
);
|
);
|
||||||
|
|
||||||
-- ── secrets ──────────────────────────────────────────────────────────────────
|
-- secrets ---------------------------------------------------------------------
|
||||||
-- Shared credentials vault. Admin write, board+ read.
|
|
||||||
-- value stored as plain text — encryption is a future enhancement.
|
|
||||||
CREATE TABLE IF NOT EXISTS public.secrets (
|
CREATE TABLE IF NOT EXISTS public.secrets (
|
||||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
space_id uuid NOT NULL REFERENCES public.spaces(id) ON DELETE CASCADE,
|
space_id uuid NOT NULL REFERENCES public.spaces(id) ON DELETE CASCADE,
|
||||||
title text NOT NULL,
|
-- label = legacy column, title = canonical
|
||||||
icon text,
|
label text NOT NULL,
|
||||||
description text,
|
title text,
|
||||||
value text NOT NULL,
|
value text NOT NULL,
|
||||||
|
description text,
|
||||||
|
category text DEFAULT 'general',
|
||||||
area text,
|
area text,
|
||||||
|
icon text,
|
||||||
|
notes text,
|
||||||
|
created_by uuid REFERENCES auth.users(id) ON DELETE SET NULL,
|
||||||
created_at timestamptz NOT NULL DEFAULT now(),
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
updated_at timestamptz NOT NULL DEFAULT now()
|
updated_at timestamptz NOT NULL DEFAULT now()
|
||||||
);
|
);
|
||||||
|
|
||||||
-- ── area_leads ───────────────────────────────────────────────────────────────
|
-- area_leads ------------------------------------------------------------------
|
||||||
-- area_code must be unique within a space (used as upsert conflict target).
|
-- area_code is the upsert conflict target (unique within a space)
|
||||||
CREATE TABLE IF NOT EXISTS public.area_leads (
|
CREATE TABLE IF NOT EXISTS public.area_leads (
|
||||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
space_id uuid NOT NULL REFERENCES public.spaces(id) ON DELETE CASCADE,
|
space_id uuid NOT NULL REFERENCES public.spaces(id) ON DELETE CASCADE,
|
||||||
area_name text NOT NULL,
|
area_name text NOT NULL,
|
||||||
area_code text NOT NULL,
|
area_code text,
|
||||||
lead_id uuid REFERENCES public.space_members(id) ON DELETE SET NULL,
|
description text,
|
||||||
|
notes text,
|
||||||
|
lead_id uuid REFERENCES public.space_members(id) ON DELETE SET NULL,
|
||||||
lead_handle text,
|
lead_handle text,
|
||||||
status public.area_lead_status NOT NULL DEFAULT 'active',
|
status public.area_lead_status NOT NULL DEFAULT 'active',
|
||||||
created_at timestamptz NOT NULL DEFAULT now(),
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||||
UNIQUE (space_id, area_code)
|
CONSTRAINT area_leads_space_area_code_key UNIQUE (space_id, area_code) DEFERRABLE INITIALLY DEFERRED
|
||||||
);
|
);
|
||||||
|
|
||||||
-- ── integrations ─────────────────────────────────────────────────────────────
|
-- integrations ----------------------------------------------------------------
|
||||||
-- config JSONB holds platform-specific credentials (e.g. client_id, token).
|
|
||||||
-- platform must be unique within a space (used as upsert conflict target).
|
|
||||||
CREATE TABLE IF NOT EXISTS public.integrations (
|
CREATE TABLE IF NOT EXISTS public.integrations (
|
||||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
space_id uuid NOT NULL REFERENCES public.spaces(id) ON DELETE CASCADE,
|
space_id uuid NOT NULL REFERENCES public.spaces(id) ON DELETE CASCADE,
|
||||||
name text NOT NULL,
|
|
||||||
platform text NOT NULL,
|
platform text NOT NULL,
|
||||||
|
name text,
|
||||||
description text,
|
description text,
|
||||||
is_connected boolean NOT NULL DEFAULT false,
|
is_connected boolean NOT NULL DEFAULT false,
|
||||||
|
-- credentials/settings kept for legacy; config is canonical
|
||||||
|
credentials jsonb DEFAULT '{}',
|
||||||
|
settings jsonb DEFAULT '{}',
|
||||||
config jsonb,
|
config jsonb,
|
||||||
|
last_sync_at timestamptz,
|
||||||
created_at timestamptz NOT NULL DEFAULT now(),
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||||
UNIQUE (space_id, platform)
|
UNIQUE (space_id, platform)
|
||||||
);
|
);
|
||||||
|
|
||||||
-- ── comms_channels ───────────────────────────────────────────────────────────
|
-- comms_channels --------------------------------------------------------------
|
||||||
-- name must be unique within a space.
|
|
||||||
CREATE TABLE IF NOT EXISTS public.comms_channels (
|
CREATE TABLE IF NOT EXISTS public.comms_channels (
|
||||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
space_id uuid NOT NULL REFERENCES public.spaces(id) ON DELETE CASCADE,
|
space_id uuid NOT NULL REFERENCES public.spaces(id) ON DELETE CASCADE,
|
||||||
name text NOT NULL,
|
name text NOT NULL,
|
||||||
|
description text,
|
||||||
|
channel_type public.channel_type NOT NULL DEFAULT 'general',
|
||||||
icon text,
|
icon text,
|
||||||
channel_type public.channel_type NOT NULL DEFAULT 'general',
|
|
||||||
area_reference text,
|
area_reference text,
|
||||||
project_id uuid REFERENCES public.projects(id) ON DELETE SET NULL,
|
is_default boolean NOT NULL DEFAULT false,
|
||||||
member_count integer NOT NULL DEFAULT 0,
|
is_archived boolean NOT NULL DEFAULT false,
|
||||||
unread_count integer NOT NULL DEFAULT 0,
|
member_count integer NOT NULL DEFAULT 0,
|
||||||
created_at timestamptz NOT NULL DEFAULT now(),
|
unread_count integer NOT NULL DEFAULT 0,
|
||||||
UNIQUE (space_id, name)
|
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 (
|
CREATE TABLE IF NOT EXISTS public.comms_messages (
|
||||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
channel_id uuid NOT NULL REFERENCES public.comms_channels(id) ON DELETE CASCADE,
|
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,
|
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,
|
||||||
display_name text NOT NULL,
|
display_name text,
|
||||||
handle text,
|
handle text,
|
||||||
content text NOT NULL,
|
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 ─────────────────────────────────────────────────────────────
|
-- activity_log ----------------------------------------------------------------
|
||||||
-- Append-only audit log. No UPDATE/DELETE policies.
|
|
||||||
CREATE TABLE IF NOT EXISTS public.activity_log (
|
CREATE TABLE IF NOT EXISTS public.activity_log (
|
||||||
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
space_id uuid NOT NULL REFERENCES public.spaces(id) ON DELETE CASCADE,
|
space_id uuid NOT NULL REFERENCES public.spaces(id) ON DELETE CASCADE,
|
||||||
@@ -284,50 +317,48 @@ CREATE TABLE IF NOT EXISTS public.activity_log (
|
|||||||
entity_type text,
|
entity_type text,
|
||||||
entity_id uuid,
|
entity_id uuid,
|
||||||
details text,
|
details text,
|
||||||
|
metadata jsonb DEFAULT '{}',
|
||||||
created_at timestamptz NOT NULL DEFAULT now()
|
created_at timestamptz NOT NULL DEFAULT now()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
-- ─────────────────────────────────────────────────────────────────────────────
|
-- -----------------------------------------------------------------------------
|
||||||
-- 4. UNIQUE CONSTRAINTS & INDEXES
|
-- 4. INDEXES
|
||||||
-- ─────────────────────────────────────────────────────────────────────────────
|
-- -----------------------------------------------------------------------------
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_space_members_user_id ON public.space_members (user_id);
|
||||||
-- Partial unique index: (space_id, email) only where email IS NOT NULL
|
CREATE INDEX IF NOT EXISTS idx_space_members_space_id ON public.space_members (space_id);
|
||||||
-- Required by importMembers upsert in actions.ts
|
CREATE INDEX IF NOT EXISTS idx_tasks_space_id ON public.tasks (space_id);
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_space_members_space_email
|
CREATE INDEX IF NOT EXISTS idx_tasks_status ON public.tasks (status);
|
||||||
ON public.space_members (space_id, email)
|
CREATE INDEX IF NOT EXISTS idx_tasks_claimed_by ON public.tasks (claimed_by);
|
||||||
WHERE email IS NOT NULL;
|
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);
|
||||||
-- Performance indexes
|
CREATE INDEX IF NOT EXISTS idx_projects_space_id ON public.projects (space_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_space_members_user_id ON public.space_members (user_id);
|
CREATE INDEX IF NOT EXISTS idx_payments_space_id ON public.payments (space_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_space_members_space_id ON public.space_members (space_id);
|
CREATE INDEX IF NOT EXISTS idx_payments_member_id ON public.payments (member_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_tasks_space_id ON public.tasks (space_id);
|
CREATE INDEX IF NOT EXISTS idx_payments_external_id ON public.payments (external_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_tasks_status ON public.tasks (status);
|
CREATE INDEX IF NOT EXISTS idx_payments_link_status ON public.payments (link_status);
|
||||||
CREATE INDEX IF NOT EXISTS idx_tasks_claimed_by ON public.tasks (claimed_by);
|
CREATE INDEX IF NOT EXISTS idx_payments_transaction_date ON public.payments (transaction_date DESC);
|
||||||
CREATE INDEX IF NOT EXISTS idx_tasks_assigned_to ON public.tasks (assigned_to);
|
CREATE INDEX IF NOT EXISTS idx_contacts_space_id ON public.contacts (space_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_projects_space_id ON public.projects (space_id);
|
CREATE INDEX IF NOT EXISTS idx_knowledge_base_space_id ON public.knowledge_base (space_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_payments_space_id ON public.payments (space_id);
|
CREATE INDEX IF NOT EXISTS idx_secrets_space_id ON public.secrets (space_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_payments_link_status ON public.payments (link_status);
|
CREATE INDEX IF NOT EXISTS idx_area_leads_space_id ON public.area_leads (space_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_payments_transaction_date ON public.payments (transaction_date DESC);
|
CREATE INDEX IF NOT EXISTS idx_integrations_space_id ON public.integrations (space_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_kb_space_id ON public.knowledge_base (space_id);
|
CREATE INDEX IF NOT EXISTS idx_comms_channels_space_id ON public.comms_channels (space_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_secrets_space_id ON public.secrets (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_contacts_space_id ON public.contacts (space_id);
|
CREATE INDEX IF NOT EXISTS idx_comms_messages_space ON public.comms_messages (space_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_area_leads_space_id ON public.area_leads (space_id);
|
CREATE INDEX IF NOT EXISTS idx_comms_messages_user ON public.comms_messages (user_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_integrations_space_id ON public.integrations (space_id);
|
CREATE INDEX IF NOT EXISTS idx_activity_log_space_id ON public.activity_log (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_activity_log_created_at ON public.activity_log (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);
|
|
||||||
|
|
||||||
|
|
||||||
-- ─────────────────────────────────────────────────────────────────────────────
|
-- -----------------------------------------------------------------------------
|
||||||
-- 5. HELPER FUNCTIONS
|
-- 5. HELPER FUNCTIONS
|
||||||
-- Must come AFTER tables because they reference space_members.
|
-- Must come after tables — they reference public.space_members.
|
||||||
-- Both are SECURITY DEFINER with fixed search_path.
|
-- SECURITY DEFINER with fixed search_path prevents privilege escalation.
|
||||||
-- ─────────────────────────────────────────────────────────────────────────────
|
-- -----------------------------------------------------------------------------
|
||||||
|
|
||||||
-- Returns all space_ids a given user belongs to.
|
-- Returns all space_ids the given user belongs to.
|
||||||
-- Used in RLS SELECT policies to avoid recursive policy evaluation.
|
-- Used in RLS SELECT policies to avoid recursive evaluation.
|
||||||
CREATE OR REPLACE FUNCTION public.get_user_space_ids(uid uuid)
|
CREATE OR REPLACE FUNCTION public.get_user_space_ids(uid uuid)
|
||||||
RETURNS SETOF uuid
|
RETURNS SETOF uuid
|
||||||
LANGUAGE sql STABLE SECURITY DEFINER
|
LANGUAGE sql STABLE SECURITY DEFINER
|
||||||
@@ -336,8 +367,7 @@ AS $$
|
|||||||
SELECT space_id FROM public.space_members WHERE user_id = uid;
|
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.
|
-- Returns true if the user holds one of the given roles in the given space.
|
||||||
-- Used in RLS policies that require elevated permissions.
|
|
||||||
CREATE OR REPLACE FUNCTION public.user_has_role_in_space(uid uuid, sid uuid, allowed_roles text[])
|
CREATE OR REPLACE FUNCTION public.user_has_role_in_space(uid uuid, sid uuid, allowed_roles text[])
|
||||||
RETURNS boolean
|
RETURNS boolean
|
||||||
LANGUAGE sql STABLE SECURITY DEFINER
|
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 $$
|
AS $$
|
||||||
SELECT EXISTS (
|
SELECT EXISTS (
|
||||||
SELECT 1 FROM public.space_members
|
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
|
-- 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.spaces ENABLE ROW LEVEL SECURITY;
|
||||||
ALTER TABLE public.space_members ENABLE ROW LEVEL SECURITY;
|
ALTER TABLE public.space_members ENABLE ROW LEVEL SECURITY;
|
||||||
ALTER TABLE public.tasks 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.comms_messages ENABLE ROW LEVEL SECURITY;
|
||||||
ALTER TABLE public.activity_log ENABLE ROW LEVEL SECURITY;
|
ALTER TABLE public.activity_log ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
-- ── spaces ───────────────────────────────────────────────────────────────────
|
-- spaces
|
||||||
CREATE POLICY "spaces_select" ON public.spaces
|
CREATE POLICY spaces_select ON public.spaces FOR SELECT
|
||||||
FOR SELECT USING (id IN (SELECT public.get_user_space_ids(auth.uid())));
|
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
|
-- space_members
|
||||||
FOR INSERT WITH CHECK (true); -- auth hook creates the space; server-side admin client used
|
CREATE POLICY members_select ON public.space_members FOR SELECT
|
||||||
|
USING (space_id IN (SELECT public.get_user_space_ids(auth.uid())));
|
||||||
CREATE POLICY "spaces_update" ON public.spaces
|
CREATE POLICY members_insert ON public.space_members FOR INSERT
|
||||||
FOR UPDATE USING (public.user_has_role_in_space(auth.uid(), id, ARRAY['admin','board']));
|
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()
|
user_id = auth.uid()
|
||||||
OR public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board','treasurer'])
|
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
|
||||||
CREATE POLICY "members_update" ON public.space_members
|
USING (
|
||||||
FOR UPDATE USING (
|
|
||||||
user_id = auth.uid()
|
user_id = auth.uid()
|
||||||
OR public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board','treasurer'])
|
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
|
-- tasks
|
||||||
FOR DELETE USING (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin']));
|
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 ────────────────────────────────────────────────────────────────────
|
-- projects
|
||||||
CREATE POLICY "tasks_select" ON public.tasks
|
CREATE POLICY projects_select ON public.projects FOR SELECT
|
||||||
FOR SELECT USING (space_id IN (SELECT public.get_user_space_ids(auth.uid())));
|
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
|
-- payments (treasurer / board / admin only)
|
||||||
FOR INSERT WITH CHECK (space_id IN (SELECT public.get_user_space_ids(auth.uid())));
|
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
|
-- contacts
|
||||||
FOR UPDATE USING (space_id IN (SELECT public.get_user_space_ids(auth.uid())));
|
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
|
-- knowledge_base
|
||||||
FOR DELETE USING (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board']));
|
CREATE POLICY kb_select ON public.knowledge_base FOR SELECT
|
||||||
|
USING (
|
||||||
-- ── 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 (
|
|
||||||
space_id IN (SELECT public.get_user_space_ids(auth.uid()))
|
space_id IN (SELECT public.get_user_space_ids(auth.uid()))
|
||||||
AND (
|
AND (
|
||||||
visibility = 'all_members'
|
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']))
|
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
|
-- secrets (board / admin only)
|
||||||
FOR INSERT WITH CHECK (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board','treasurer']));
|
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
|
-- area_leads
|
||||||
FOR UPDATE USING (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board','treasurer']));
|
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
|
-- integrations (admin only)
|
||||||
FOR DELETE USING (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board']));
|
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 ──────────────────────────────────────────────────────────────────
|
-- comms_channels
|
||||||
CREATE POLICY "secrets_select" ON public.secrets
|
CREATE POLICY channels_select ON public.comms_channels FOR SELECT
|
||||||
FOR SELECT USING (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board']));
|
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
|
-- comms_messages
|
||||||
FOR INSERT WITH CHECK (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board']));
|
CREATE POLICY messages_select ON public.comms_messages FOR SELECT
|
||||||
|
USING (space_id IN (SELECT public.get_user_space_ids(auth.uid())));
|
||||||
CREATE POLICY "secrets_update" ON public.secrets
|
CREATE POLICY messages_insert ON public.comms_messages FOR INSERT
|
||||||
FOR UPDATE USING (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board']));
|
WITH CHECK (
|
||||||
|
|
||||||
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 (
|
|
||||||
space_id IN (SELECT public.get_user_space_ids(auth.uid()))
|
space_id IN (SELECT public.get_user_space_ids(auth.uid()))
|
||||||
AND user_id = auth.uid()
|
AND user_id = auth.uid()
|
||||||
);
|
);
|
||||||
|
CREATE POLICY messages_update ON public.comms_messages FOR UPDATE
|
||||||
CREATE POLICY "messages_update" ON public.comms_messages
|
USING (
|
||||||
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()
|
user_id = auth.uid()
|
||||||
OR public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board'])
|
OR public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board'])
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE POLICY "messages_delete" ON public.comms_messages
|
-- activity_log (append-only for members, readable by admin/board)
|
||||||
FOR DELETE USING (
|
CREATE POLICY activity_select ON public.activity_log FOR SELECT
|
||||||
user_id = auth.uid()
|
USING (public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board']));
|
||||||
OR public.user_has_role_in_space(auth.uid(), space_id, ARRAY['admin','board'])
|
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 ─────────────────────────────────────────────────────────────
|
|
||||||
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
|
-- 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()
|
CREATE OR REPLACE FUNCTION public.create_default_channels()
|
||||||
RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER
|
RETURNS trigger
|
||||||
|
LANGUAGE plpgsql SECURITY DEFINER
|
||||||
SET search_path = public
|
SET search_path = public
|
||||||
AS $$
|
AS $$
|
||||||
BEGIN
|
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
|
VALUES
|
||||||
(NEW.id, 'general', '💬', 'general'),
|
(NEW.id, 'general', 'General discussion', 'general', true),
|
||||||
(NEW.id, 'announcements', '📢', 'general'),
|
(NEW.id, 'announcements', 'Important announcements', 'general', false),
|
||||||
(NEW.id, 'ops', '⚙️', 'ops');
|
(NEW.id, 'ops', 'Operations & logistics', 'ops', false);
|
||||||
RETURN NEW;
|
RETURN NEW;
|
||||||
END;
|
END;
|
||||||
$$;
|
$$;
|
||||||
@@ -581,37 +571,44 @@ CREATE TRIGGER on_space_created
|
|||||||
AFTER INSERT ON public.spaces
|
AFTER INSERT ON public.spaces
|
||||||
FOR EACH ROW EXECUTE FUNCTION public.create_default_channels();
|
FOR EACH ROW EXECUTE FUNCTION public.create_default_channels();
|
||||||
|
|
||||||
-- Auth hook: create a space_member row when a new user signs up.
|
-- Auth signup hook
|
||||||
-- Reads metadata fields: space_id, role, full_name / display_name.
|
-- Called when a new user is created in auth.users.
|
||||||
-- NOTE: The app's signup flow calls createSpace/joinSpace server actions
|
-- Reads metadata from supabase.auth.signUp({ data: { space_id, role, display_name } })
|
||||||
-- directly after signUp() returns a session (email confirm disabled).
|
-- and inserts the corresponding space_members row.
|
||||||
-- This trigger is a safety net for when email confirmation IS enabled.
|
|
||||||
CREATE OR REPLACE FUNCTION public.handle_space_signup()
|
CREATE OR REPLACE FUNCTION public.handle_space_signup()
|
||||||
RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER
|
RETURNS trigger
|
||||||
|
LANGUAGE plpgsql SECURITY DEFINER
|
||||||
SET search_path = public
|
SET search_path = public
|
||||||
AS $$
|
AS $$
|
||||||
DECLARE
|
DECLARE
|
||||||
v_space_id uuid;
|
v_space_id uuid;
|
||||||
v_role public.member_role;
|
v_role public.member_role;
|
||||||
v_display_name text;
|
v_display_name text;
|
||||||
BEGIN
|
BEGIN
|
||||||
v_space_id := (NEW.raw_user_meta_data->>'space_id')::uuid;
|
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
|
|
||||||
);
|
|
||||||
|
|
||||||
|
-- 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
|
IF v_space_id IS NULL THEN
|
||||||
RETURN NEW;
|
RETURN NEW;
|
||||||
END IF;
|
END IF;
|
||||||
|
|
||||||
INSERT INTO public.space_members (space_id, user_id, role, status, display_name, email, approved)
|
INSERT INTO public.space_members
|
||||||
VALUES (v_space_id, NEW.id, v_role, 'unverified', v_display_name, NEW.email, true)
|
(space_id, user_id, 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;
|
ON CONFLICT (space_id, user_id) DO NOTHING;
|
||||||
|
|
||||||
RETURN NEW;
|
RETURN NEW;
|
||||||
@@ -624,9 +621,8 @@ CREATE TRIGGER on_auth_user_created
|
|||||||
FOR EACH ROW EXECUTE FUNCTION public.handle_space_signup();
|
FOR EACH ROW EXECUTE FUNCTION public.handle_space_signup();
|
||||||
|
|
||||||
|
|
||||||
-- ─────────────────────────────────────────────────────────────────────────────
|
-- -----------------------------------------------------------------------------
|
||||||
-- 8. REALTIME
|
-- 8. REALTIME
|
||||||
-- ─────────────────────────────────────────────────────────────────────────────
|
-- -----------------------------------------------------------------------------
|
||||||
-- Enable realtime for live chat.
|
|
||||||
ALTER PUBLICATION supabase_realtime ADD TABLE public.comms_messages;
|
ALTER PUBLICATION supabase_realtime ADD TABLE public.comms_messages;
|
||||||
ALTER PUBLICATION supabase_realtime ADD TABLE public.comms_channels;
|
ALTER PUBLICATION supabase_realtime ADD TABLE public.comms_channels;
|
||||||
|
|||||||
Reference in New Issue
Block a user