Merge pull request #2 from virgilvox/v0/hackbuildvideo-3367-4dea679b

Fix database schema mismatches and improve authentication flow
This commit is contained in:
vercel[bot]
2026-03-11 06:59:04 +00:00
committed by GitHub
19 changed files with 503 additions and 2232 deletions
+7 -14
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
-37
View File
@@ -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;
-635
View File
@@ -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);
-82
View File
@@ -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();
-9
View File
@@ -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';
-85
View File
@@ -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();
-39
View File
@@ -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()
);
-20
View File
@@ -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()
);
-516
View File
@@ -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;
-62
View File
@@ -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;
-12
View File
@@ -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;
-32
View File
@@ -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 $$;
-27
View File
@@ -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();
-93
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff