Initial commit from v0
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
-- 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;
|
||||
@@ -0,0 +1,635 @@
|
||||
-- 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);
|
||||
@@ -0,0 +1,82 @@
|
||||
-- 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();
|
||||
@@ -0,0 +1,9 @@
|
||||
-- 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';
|
||||
@@ -0,0 +1,85 @@
|
||||
-- =====================================================
|
||||
-- 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();
|
||||
@@ -0,0 +1,39 @@
|
||||
-- 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()
|
||||
);
|
||||
@@ -0,0 +1,20 @@
|
||||
-- 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()
|
||||
);
|
||||
@@ -0,0 +1,516 @@
|
||||
-- =====================================================
|
||||
-- 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;
|
||||
@@ -0,0 +1,62 @@
|
||||
-- =====================================================
|
||||
-- 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;
|
||||
@@ -0,0 +1,12 @@
|
||||
-- 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;
|
||||
@@ -0,0 +1,32 @@
|
||||
-- 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 $$;
|
||||
@@ -0,0 +1,27 @@
|
||||
-- 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();
|
||||
@@ -0,0 +1,93 @@
|
||||
-- =====================================================
|
||||
-- 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';
|
||||
@@ -0,0 +1,35 @@
|
||||
import { readFileSync, writeFileSync } from 'fs'
|
||||
|
||||
const path = '/vercel/share/v0-project/app/(app)/dashboard/page.tsx'
|
||||
let content = readFileSync(path, 'utf8')
|
||||
|
||||
// 1. Fix the payments query
|
||||
content = content.replace(
|
||||
`.is('linked_member_id', null)`,
|
||||
`.eq('link_status', 'unlinked')`
|
||||
)
|
||||
|
||||
// 2. Add ClaimButton import after the redirect import
|
||||
if (!content.includes('ClaimButton')) {
|
||||
content = content.replace(
|
||||
"import { redirect } from 'next/navigation'",
|
||||
"import { redirect } from 'next/navigation'\nimport { ClaimButton } from '@/components/task-claim-button'"
|
||||
)
|
||||
}
|
||||
|
||||
// 3. Replace the static CLAIM link with ClaimButton
|
||||
content = content.replace(
|
||||
/<Link href="\/tasks" className="font-mono text-\[10px\] border border-border px-2 py-0\.5 rounded hover:border-primary hover:text-primary transition">CLAIM<\/Link>/,
|
||||
'{task.status === \'open\' && <ClaimButton taskId={task.id} />}'
|
||||
)
|
||||
|
||||
// 4. Fix project.name -> project.title
|
||||
content = content.replace(/project\.name\b/g, 'project.title')
|
||||
|
||||
writeFileSync(path, content, 'utf8')
|
||||
console.log('[v0] Dashboard patched successfully')
|
||||
console.log('[v0] Content preview (lines 1-10):')
|
||||
content.split('\n').slice(0, 10).forEach((line, i) => console.log(`${i+1}: ${line}`))
|
||||
console.log('[v0] Check linked_member_id still present:', content.includes('linked_member_id'))
|
||||
console.log('[v0] Check link_status present:', content.includes('link_status'))
|
||||
console.log('[v0] Check ClaimButton import:', content.includes('ClaimButton'))
|
||||
Reference in New Issue
Block a user