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