-- 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') ) );