322 lines
9.1 KiB
PL/PgSQL
322 lines
9.1 KiB
PL/PgSQL
-- Fresh reset migration: replace collaboration IDs with lowercase hash-like text IDs.
|
|
-- No compatibility shims by design.
|
|
|
|
create extension if not exists pgcrypto;
|
|
|
|
-- Drop existing collaboration objects.
|
|
drop table if exists public.messages cascade;
|
|
drop table if exists public.channel_members cascade;
|
|
drop table if exists public.channels cascade;
|
|
drop table if exists public.organization_members cascade;
|
|
drop table if exists public.organizations cascade;
|
|
|
|
drop function if exists public.can_access_channel(uuid, uuid);
|
|
drop function if exists public.org_role(uuid, uuid);
|
|
drop function if exists public.is_org_member(uuid, uuid);
|
|
drop function if exists public.can_access_channel(text, uuid);
|
|
drop function if exists public.org_role(text, uuid);
|
|
drop function if exists public.is_org_member(text, uuid);
|
|
|
|
-- Keep enum types if already present.
|
|
do $$
|
|
begin
|
|
if not exists (select 1 from pg_type where typname = 'organization_role') then
|
|
create type public.organization_role as enum ('owner', 'admin', 'member');
|
|
end if;
|
|
if not exists (select 1 from pg_type where typname = 'channel_type') then
|
|
create type public.channel_type as enum ('text', 'voice', 'announcement');
|
|
end if;
|
|
end $$;
|
|
|
|
create or replace function public.gen_hash_id()
|
|
returns text
|
|
language sql
|
|
volatile
|
|
as $$
|
|
select substring(md5(random()::text || clock_timestamp()::text) from 1 for 16);
|
|
$$;
|
|
|
|
create table public.organizations (
|
|
id text primary key default public.gen_hash_id() check (id ~ '^[0-9a-f]{16}$'),
|
|
name text not null,
|
|
slug text not null unique,
|
|
owner_user_id uuid not null references auth.users(id) on delete restrict,
|
|
created_at timestamptz not null default now()
|
|
);
|
|
|
|
create table public.organization_members (
|
|
organization_id text not null references public.organizations(id) on delete cascade,
|
|
user_id uuid not null references auth.users(id) on delete cascade,
|
|
role public.organization_role not null default 'member',
|
|
joined_at timestamptz not null default now(),
|
|
primary key (organization_id, user_id)
|
|
);
|
|
|
|
create table public.channels (
|
|
id text primary key default public.gen_hash_id() check (id ~ '^[0-9a-f]{16}$'),
|
|
organization_id text not null references public.organizations(id) on delete cascade,
|
|
name text not null,
|
|
slug text not null,
|
|
type public.channel_type not null default 'text',
|
|
position integer not null default 0,
|
|
topic text,
|
|
is_private boolean not null default false,
|
|
created_by uuid not null references auth.users(id) on delete restrict,
|
|
created_at timestamptz not null default now(),
|
|
unique (organization_id, slug)
|
|
);
|
|
|
|
create table public.channel_members (
|
|
channel_id text not null references public.channels(id) on delete cascade,
|
|
user_id uuid not null references auth.users(id) on delete cascade,
|
|
joined_at timestamptz not null default now(),
|
|
primary key (channel_id, user_id)
|
|
);
|
|
|
|
create table public.messages (
|
|
id text primary key default public.gen_hash_id() check (id ~ '^[0-9a-f]{16}$'),
|
|
channel_id text not null references public.channels(id) on delete cascade,
|
|
author_user_id uuid not null references auth.users(id) on delete restrict,
|
|
content text not null check (char_length(content) <= 4000),
|
|
created_at timestamptz not null default now(),
|
|
edited_at timestamptz,
|
|
deleted_at timestamptz
|
|
);
|
|
|
|
create index idx_org_members_user on public.organization_members(user_id);
|
|
create index idx_channels_org_position on public.channels(organization_id, position);
|
|
create index idx_messages_channel_created_at_desc on public.messages(channel_id, created_at desc);
|
|
|
|
create or replace function public.is_org_member(org_id text, uid uuid default auth.uid())
|
|
returns boolean
|
|
language sql
|
|
stable
|
|
security definer
|
|
set search_path = public
|
|
as $$
|
|
select exists (
|
|
select 1
|
|
from public.organization_members om
|
|
where om.organization_id = org_id
|
|
and om.user_id = uid
|
|
);
|
|
$$;
|
|
|
|
create or replace function public.org_role(org_id text, uid uuid default auth.uid())
|
|
returns public.organization_role
|
|
language sql
|
|
stable
|
|
security definer
|
|
set search_path = public
|
|
as $$
|
|
select om.role
|
|
from public.organization_members om
|
|
where om.organization_id = org_id
|
|
and om.user_id = uid
|
|
limit 1;
|
|
$$;
|
|
|
|
create or replace function public.can_access_channel(ch_id text, uid uuid default auth.uid())
|
|
returns boolean
|
|
language sql
|
|
stable
|
|
security definer
|
|
set search_path = public
|
|
as $$
|
|
select exists (
|
|
select 1
|
|
from public.channels c
|
|
where c.id = ch_id
|
|
and public.is_org_member(c.organization_id, uid)
|
|
and (
|
|
c.is_private = false
|
|
or exists (
|
|
select 1
|
|
from public.channel_members cm
|
|
where cm.channel_id = c.id
|
|
and cm.user_id = uid
|
|
)
|
|
)
|
|
);
|
|
$$;
|
|
|
|
alter table public.organizations enable row level security;
|
|
alter table public.organization_members enable row level security;
|
|
alter table public.channels enable row level security;
|
|
alter table public.channel_members enable row level security;
|
|
alter table public.messages enable row level security;
|
|
|
|
create policy "organizations_select_members"
|
|
on public.organizations
|
|
for select
|
|
to authenticated
|
|
using (public.is_org_member(id));
|
|
|
|
create policy "organizations_insert_owner"
|
|
on public.organizations
|
|
for insert
|
|
to authenticated
|
|
with check (owner_user_id = auth.uid());
|
|
|
|
create policy "organizations_update_admins"
|
|
on public.organizations
|
|
for update
|
|
to authenticated
|
|
using (public.org_role(id) in ('owner', 'admin'))
|
|
with check (public.org_role(id) in ('owner', 'admin'));
|
|
|
|
create policy "organization_members_select_members"
|
|
on public.organization_members
|
|
for select
|
|
to authenticated
|
|
using (public.is_org_member(organization_id));
|
|
|
|
create policy "organization_members_insert_admins"
|
|
on public.organization_members
|
|
for insert
|
|
to authenticated
|
|
with check (
|
|
user_id = auth.uid()
|
|
and (
|
|
public.org_role(organization_id) in ('owner', 'admin')
|
|
or exists (
|
|
select 1
|
|
from public.organizations o
|
|
where o.id = organization_id
|
|
and o.owner_user_id = auth.uid()
|
|
)
|
|
)
|
|
);
|
|
|
|
create policy "organization_members_update_admins"
|
|
on public.organization_members
|
|
for update
|
|
to authenticated
|
|
using (public.org_role(organization_id) in ('owner', 'admin'))
|
|
with check (public.org_role(organization_id) in ('owner', 'admin'));
|
|
|
|
create policy "organization_members_delete_admins"
|
|
on public.organization_members
|
|
for delete
|
|
to authenticated
|
|
using (public.org_role(organization_id) in ('owner', 'admin'));
|
|
|
|
create policy "channels_select_visible"
|
|
on public.channels
|
|
for select
|
|
to authenticated
|
|
using (public.can_access_channel(id));
|
|
|
|
create policy "channels_insert_admins"
|
|
on public.channels
|
|
for insert
|
|
to authenticated
|
|
with check (
|
|
public.org_role(organization_id) in ('owner', 'admin')
|
|
and created_by = auth.uid()
|
|
);
|
|
|
|
create policy "channels_update_admins"
|
|
on public.channels
|
|
for update
|
|
to authenticated
|
|
using (public.org_role(organization_id) in ('owner', 'admin'))
|
|
with check (public.org_role(organization_id) in ('owner', 'admin'));
|
|
|
|
create policy "channels_delete_admins"
|
|
on public.channels
|
|
for delete
|
|
to authenticated
|
|
using (public.org_role(organization_id) in ('owner', 'admin'));
|
|
|
|
create policy "channel_members_select_visible"
|
|
on public.channel_members
|
|
for select
|
|
to authenticated
|
|
using (
|
|
exists (
|
|
select 1
|
|
from public.channels c
|
|
where c.id = channel_id
|
|
and public.is_org_member(c.organization_id)
|
|
)
|
|
);
|
|
|
|
create policy "channel_members_insert_admins"
|
|
on public.channel_members
|
|
for insert
|
|
to authenticated
|
|
with check (
|
|
exists (
|
|
select 1
|
|
from public.channels c
|
|
where c.id = channel_id
|
|
and public.org_role(c.organization_id) in ('owner', 'admin')
|
|
)
|
|
);
|
|
|
|
create policy "channel_members_delete_admins"
|
|
on public.channel_members
|
|
for delete
|
|
to authenticated
|
|
using (
|
|
exists (
|
|
select 1
|
|
from public.channels c
|
|
where c.id = channel_id
|
|
and public.org_role(c.organization_id) in ('owner', 'admin')
|
|
)
|
|
);
|
|
|
|
create policy "messages_select_visible_channel"
|
|
on public.messages
|
|
for select
|
|
to authenticated
|
|
using (public.can_access_channel(channel_id));
|
|
|
|
create policy "messages_insert_visible_channel"
|
|
on public.messages
|
|
for insert
|
|
to authenticated
|
|
with check (
|
|
public.can_access_channel(channel_id)
|
|
and author_user_id = auth.uid()
|
|
and deleted_at is null
|
|
);
|
|
|
|
create policy "messages_update_author_or_admin"
|
|
on public.messages
|
|
for update
|
|
to authenticated
|
|
using (
|
|
author_user_id = auth.uid()
|
|
or exists (
|
|
select 1
|
|
from public.channels c
|
|
where c.id = channel_id
|
|
and public.org_role(c.organization_id) in ('owner', 'admin')
|
|
)
|
|
)
|
|
with check (
|
|
author_user_id = auth.uid()
|
|
or exists (
|
|
select 1
|
|
from public.channels c
|
|
where c.id = channel_id
|
|
and public.org_role(c.organization_id) in ('owner', 'admin')
|
|
)
|
|
);
|
|
|
|
create policy "messages_delete_author_or_admin"
|
|
on public.messages
|
|
for delete
|
|
to authenticated
|
|
using (
|
|
author_user_id = auth.uid()
|
|
or exists (
|
|
select 1
|
|
from public.channels c
|
|
where c.id = channel_id
|
|
and public.org_role(c.organization_id) in ('owner', 'admin')
|
|
)
|
|
);
|