486 lines
16 KiB
PL/PgSQL
486 lines
16 KiB
PL/PgSQL
--
|
|
-- For use with https://github.com/supabase/supabase/tree/master/examples/slack-clone/nextjs-slack-clone
|
|
--
|
|
|
|
-- Custom types
|
|
create type public.app_permission as enum ('events.see.hidden', 'users.ban');
|
|
create type public.app_role as enum ('sa', 'admin', 'moderator', 'user');
|
|
create type public.app_colors as enum ('red', 'orange', 'amber', 'yellow', 'lime', 'green', 'emerald', 'teal', 'cyan', 'sky', 'blue', 'indigo', 'violet', 'purple', 'fuchsia', 'pink', 'rose', 'black', 'white');
|
|
|
|
create type public.world_state as enum ('published', 'draft', 'archived');
|
|
create type public.calendar_state as enum ('published', 'draft', 'archived');
|
|
|
|
-- DATA STRUCTURES
|
|
-- Users
|
|
create table public.users (
|
|
id uuid references auth.users not null primary key, -- UUID from auth.users
|
|
username text
|
|
);
|
|
comment on table public.users is 'Profile data for each user.';
|
|
comment on column public.users.id is 'References the internal Supabase Auth user.';
|
|
|
|
-- Roles
|
|
create table public.user_roles (
|
|
id bigint generated by default as identity primary key,
|
|
role app_role not null,
|
|
user_id uuid references public.users on delete cascade not null,
|
|
unique (user_id, role)
|
|
);
|
|
comment on table public.user_roles is 'Application roles for each user.';
|
|
|
|
-- Permissions
|
|
create table public.role_permissions (
|
|
id bigint generated by default as identity primary key,
|
|
role app_role not null,
|
|
permission app_permission not null,
|
|
unique (role, permission)
|
|
);
|
|
comment on table public.role_permissions is 'Application permissions for each role.';
|
|
|
|
-- Worlds
|
|
create table public.worlds (
|
|
id bigint generated by default as identity primary key,
|
|
name text not null,
|
|
description text,
|
|
color app_colors default 'black',
|
|
state world_state default 'draft',
|
|
gm_id uuid references public.users on delete cascade
|
|
);
|
|
comment on table public.worlds is 'Worlds belonging to a single user ; a game master.';
|
|
|
|
-- World Players (join table)
|
|
create table public.world_players (
|
|
world_id bigint references public.worlds on delete cascade,
|
|
player_id uuid references public.users on delete cascade,
|
|
joined_at timestamptz default now(),
|
|
primary key (world_id, player_id)
|
|
);
|
|
comment on table public.world_players is 'Players (users) that belong to specific worlds';
|
|
comment on column public.world_players.joined_at is 'Timestamp when the player joined the world';
|
|
|
|
-- World Calendars
|
|
create table public.calendars (
|
|
id bigint generated by default as identity primary key,
|
|
short_id text unique,
|
|
name text not null,
|
|
today json not null,
|
|
color app_colors default 'black',
|
|
state calendar_state default 'draft',
|
|
world_id bigint references public.worlds on delete cascade not null
|
|
);
|
|
comment on table public.calendars is 'Calendar settings and configuration attached to a single world.';
|
|
|
|
-- Calendar Months
|
|
create table public.calendar_months (
|
|
id bigint generated by default as identity primary key,
|
|
name text not null,
|
|
days int not null,
|
|
position int not null,
|
|
calendar_id bigint references public.calendars on delete cascade not null
|
|
);
|
|
comment on table public.calendar_months is 'A calendar month.';
|
|
|
|
-- Calendar Event categories
|
|
create table public.calendar_event_categories (
|
|
id bigint generated by default as identity primary key,
|
|
name text not null,
|
|
color app_colors default 'black',
|
|
unique (name)
|
|
);
|
|
comment on table public.calendar_event_categories is 'Categories describing events.';
|
|
|
|
-- Calendar Events
|
|
create table public.calendar_events (
|
|
id bigint generated by default as identity primary key,
|
|
title text not null,
|
|
description text,
|
|
location text,
|
|
start_date json not null,
|
|
end_date json,
|
|
category bigint references public.calendar_event_categories on delete cascade,
|
|
hidden boolean default false,
|
|
wiki text,
|
|
calendar_id bigint references public.calendars on delete cascade not null
|
|
);
|
|
comment on table public.calendar_events is 'Events linked to a world';
|
|
|
|
-- Calendar Events restrictions
|
|
alter table public.calendar_events
|
|
add constraint calendar_events_maxlen_check
|
|
check (
|
|
char_length(title) <= 120 AND
|
|
(description IS NULL OR char_length(description) <= 1200) AND
|
|
(location IS NULL OR char_length(location) <= 160)
|
|
);
|
|
|
|
-- Link table for events - categories
|
|
create table public.calendar_event_categories_links (
|
|
calendar_event_id bigint references public.calendar_events on delete cascade,
|
|
calendar_event_category_id bigint references public.calendar_event_categories on delete cascade,
|
|
primary key (calendar_event_id, calendar_event_category_id)
|
|
);
|
|
comment on table public.calendar_event_categories_links is 'Link tables for multiple event categories.';
|
|
|
|
-- Character categories
|
|
create table public.character_categories (
|
|
id bigint generated by default as identity primary key,
|
|
name text not null,
|
|
color app_colors default 'black',
|
|
unique (name)
|
|
);
|
|
comment on table public.character_categories is 'Categories describing characters';
|
|
|
|
-- Characters
|
|
create table public.characters (
|
|
id bigint generated by default as identity primary key,
|
|
name text not null,
|
|
description text,
|
|
birth json not null,
|
|
death json,
|
|
category bigint references public.character_categories on delete cascade,
|
|
hidden_birth boolean,
|
|
hidden_death boolean,
|
|
wiki text,
|
|
world_id bigint references public.worlds on delete cascade not null
|
|
);
|
|
comment on table public.characters is 'Characters linked to a world';
|
|
|
|
-- Link table for events - categories
|
|
create table public.character_categories_links (
|
|
character_id bigint references public.characters on delete cascade,
|
|
character_category_id bigint references public.character_categories on delete cascade,
|
|
primary key (character_id, character_category_id)
|
|
);
|
|
comment on table public.character_categories_links is 'Link tables for multiple character categories.';
|
|
|
|
-- authorize with role-based access control (RBAC)
|
|
create function public.authorize(
|
|
requested_permission app_permission
|
|
)
|
|
returns boolean as $$
|
|
declare
|
|
bind_permissions int;
|
|
begin
|
|
select count(*)
|
|
from public.role_permissions
|
|
where role_permissions.permission = authorize.requested_permission
|
|
and role_permissions.role = (auth.jwt() ->> 'user_role')::public.app_role
|
|
into bind_permissions;
|
|
|
|
return bind_permissions > 0;
|
|
end;
|
|
$$ language plpgsql security definer set search_path = public;
|
|
|
|
-- Secure the tables
|
|
alter table public.users enable row level security;
|
|
alter table public.user_roles enable row level security;
|
|
alter table public.role_permissions enable row level security;
|
|
alter table public.worlds enable row level security;
|
|
alter table public.world_players enable row level security;
|
|
alter table public.character_categories enable row level security;
|
|
alter table public.character_categories_links enable row level security;
|
|
alter table public.characters enable row level security;
|
|
alter table public.calendars enable row level security;
|
|
alter table public.calendar_months enable row level security;
|
|
alter table public.calendar_events enable row level security;
|
|
alter table public.calendar_event_categories enable row level security;
|
|
alter table public.calendar_event_categories_links enable row level security;
|
|
|
|
-- Add realtime
|
|
alter publication supabase_realtime add table users;
|
|
alter publication supabase_realtime add table worlds;
|
|
alter publication supabase_realtime add table calendars;
|
|
|
|
-- User policies
|
|
create policy "Allow public read access" on public.users for select to authenticated, anon using ( true );
|
|
create policy "Allow logged-in read access" on public.users for select using ( auth.role() = 'authenticated' );
|
|
create policy "Allow individual insert access" on public.users for insert with check ( auth.uid() = id );
|
|
create policy "Allow individual update access" on public.users for update using ( auth.uid() = id );
|
|
create policy "Allow individual read access" on public.user_roles for select using ( auth.uid() = user_id );
|
|
|
|
-- World policies
|
|
create policy "Allow anonymous access to published worlds" on public.worlds
|
|
for select
|
|
using (state = 'published');
|
|
create policy "Allow GMs to see their worlds" on public.worlds for select using ( ( auth.uid() = gm_id ) );
|
|
create policy "Allow GMs to create worlds" on public.worlds for insert with check ( auth.uid() = gm_id );
|
|
create policy "Allow GMs to edit their worlds" on public.worlds for update using ( auth.uid() = gm_id );
|
|
create policy "Allow GMs to delete their worlds" on public.worlds for delete using ( auth.uid() = gm_id );
|
|
|
|
-- Calendar policies
|
|
create policy "Allow anonymous access to published calendars" on public.calendars
|
|
for select
|
|
using (state = 'published');
|
|
|
|
create policy "Allow GMs to see their calendars" on public.calendars for select using (
|
|
exists (
|
|
select 1 from worlds
|
|
where worlds.id = calendars.world_id
|
|
)
|
|
);
|
|
create policy "Allow GMs to add calendars to their worldd" on public.calendars for insert with check (
|
|
exists (
|
|
select 1 from worlds
|
|
where worlds.id = calendars.world_id
|
|
)
|
|
);
|
|
create policy "Allow GMs to edit their calendars" on public.calendars for update with check (
|
|
exists (
|
|
select 1 from worlds
|
|
where worlds.id = calendars.world_id
|
|
)
|
|
);
|
|
create policy "Allow GMs to delete their calendars" on public.calendars for delete using (
|
|
exists (
|
|
select 1 from worlds
|
|
where worlds.id = calendars.world_id
|
|
)
|
|
);
|
|
|
|
-- Month policies
|
|
create policy "Allow anonymous access to months in published calendars" ON public.calendar_months
|
|
for select
|
|
using (
|
|
exists (
|
|
select 1
|
|
from public.calendars
|
|
where calendars.id = calendar_months.calendar_id
|
|
and calendars.state = 'published'
|
|
)
|
|
);
|
|
|
|
create policy "Allow GMs to see their calendar's months"
|
|
on public.calendar_months
|
|
for select
|
|
using (
|
|
exists (
|
|
select 1
|
|
from public.calendars c
|
|
join public.worlds w on w.id = c.world_id
|
|
where
|
|
c.id = calendar_months.calendar_id
|
|
and w.gm_id = auth.uid()
|
|
)
|
|
);
|
|
create policy "Allow GMs to add months on their calendars"
|
|
on public.calendar_months
|
|
for insert
|
|
with check (
|
|
exists (
|
|
select 1
|
|
from public.calendars c
|
|
join public.worlds w on w.id = c.world_id
|
|
where
|
|
c.id = calendar_months.calendar_id
|
|
and w.gm_id = auth.uid()
|
|
)
|
|
);
|
|
create policy "Allow GMs to update their calendar's months"
|
|
on public.calendar_months
|
|
for update
|
|
using (
|
|
exists (
|
|
select 1
|
|
from public.calendars c
|
|
join public.worlds w on w.id = c.world_id
|
|
where
|
|
c.id = calendar_months.calendar_id
|
|
and w.gm_id = auth.uid()
|
|
)
|
|
);
|
|
create policy "Allow GMs to delete their calendar's months"
|
|
on public.calendar_months
|
|
for delete
|
|
using (
|
|
exists (
|
|
select 1
|
|
from public.calendars c
|
|
join public.worlds w on w.id = c.world_id
|
|
where
|
|
c.id = calendar_months.calendar_id
|
|
and w.gm_id = auth.uid()
|
|
)
|
|
);
|
|
|
|
-- Event policies
|
|
create policy "Allow anonymous access to non-hidden events in published calendars" ON public.calendar_events
|
|
for select
|
|
using (
|
|
not hidden and exists (
|
|
select 1
|
|
from public.calendars
|
|
where calendars.id = calendar_events.calendar_id
|
|
and calendars.state = 'published'
|
|
)
|
|
);
|
|
|
|
create policy "Allow GMs to see their events"
|
|
on public.calendar_events
|
|
for select
|
|
using (
|
|
exists (
|
|
select 1
|
|
from public.calendars c
|
|
join public.worlds w on w.id = c.world_id
|
|
where
|
|
c.id = calendar_events.calendar_id
|
|
and w.gm_id = auth.uid()
|
|
)
|
|
);
|
|
create policy "Allow GMs to add their events"
|
|
on public.calendar_events
|
|
for insert
|
|
with check (
|
|
exists (
|
|
select 1
|
|
from public.calendars c
|
|
join public.worlds w on w.id = c.world_id
|
|
where
|
|
c.id = calendar_events.calendar_id
|
|
and w.gm_id = auth.uid()
|
|
)
|
|
);
|
|
create policy "Allow GMs to update their events"
|
|
on public.calendar_events
|
|
for update
|
|
using (
|
|
exists (
|
|
select 1
|
|
from public.calendars c
|
|
join public.worlds w on w.id = c.world_id
|
|
where
|
|
c.id = calendar_events.calendar_id
|
|
and w.gm_id = auth.uid()
|
|
)
|
|
);
|
|
create policy "Allow GMs to delete their events"
|
|
on public.calendar_events
|
|
for delete
|
|
using (
|
|
exists (
|
|
select 1
|
|
from public.calendars c
|
|
join public.worlds w on w.id = c.world_id
|
|
where
|
|
c.id = calendar_events.calendar_id
|
|
and w.gm_id = auth.uid()
|
|
)
|
|
);
|
|
|
|
-- Character policies
|
|
create policy "Allow individual read access for GMs" on public.characters for select using (
|
|
exists (
|
|
select 1 from worlds
|
|
where worlds.id = characters.world_id
|
|
)
|
|
);
|
|
create policy "Allow individual insert access for GMs" on public.characters for insert with check (
|
|
exists (
|
|
select 1 from worlds
|
|
where worlds.id = characters.world_id
|
|
)
|
|
);
|
|
create policy "Allow individual update access for GMs" on public.characters for update with check (
|
|
exists (
|
|
select 1 from worlds
|
|
where worlds.id = characters.world_id
|
|
)
|
|
);
|
|
|
|
-- Categories are public to view but not to insert
|
|
-- Needs to be refactored maybe, if in the future we want a default set AND user defined ones
|
|
create policy "Allow all read access" on public.calendar_event_categories for select to authenticated, anon using ( true );
|
|
create policy "Allow all read access" on public.calendar_event_categories_links for select to authenticated, anon using ( true );
|
|
create policy "Allow logged-in read access" on public.character_categories for select using ( auth.role() = 'authenticated' );
|
|
create policy "Allow logged-in read access" on public.character_categories_links for select using ( auth.role() = 'authenticated' );
|
|
|
|
-- Send "previous data" on change
|
|
alter table public.users replica identity full;
|
|
|
|
-- inserts a row into public.users and assigns roles
|
|
create function public.handle_new_user()
|
|
returns trigger as $$
|
|
declare is_admin boolean;
|
|
begin
|
|
insert into public.users (id, username)
|
|
values (new.id, new.email);
|
|
insert into public.user_roles (role, user_id)
|
|
values ('user', new.id);
|
|
|
|
return new;
|
|
end;
|
|
$$ language plpgsql security definer set search_path = auth, public;
|
|
|
|
-- trigger the function every time a user is created
|
|
create trigger on_auth_user_created
|
|
after insert on auth.users
|
|
for each row execute procedure public.handle_new_user();
|
|
|
|
/**
|
|
* HELPER FUNCTIONS
|
|
* Create test user helper method.
|
|
*/
|
|
create or replace function public.create_user(
|
|
email text
|
|
) returns uuid
|
|
security definer
|
|
set search_path = auth
|
|
as $$
|
|
declare
|
|
user_id uuid;
|
|
begin
|
|
user_id := extensions.uuid_generate_v4();
|
|
|
|
insert into auth.users (id, email)
|
|
values (user_id, email)
|
|
returning id into user_id;
|
|
|
|
return user_id;
|
|
end;
|
|
$$ language plpgsql;
|
|
|
|
-- Add short IDs to calendars
|
|
-- Function to generate YouTube-like IDs
|
|
CREATE OR REPLACE FUNCTION public.generate_short_id()
|
|
RETURNS text AS $$
|
|
DECLARE
|
|
characters text := 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
|
|
result text := '';
|
|
i integer;
|
|
BEGIN
|
|
FOR i IN 1..11 LOOP
|
|
result := result || substr(characters, floor(random() * length(characters) + 1)::integer, 1);
|
|
END LOOP;
|
|
RETURN result;
|
|
END;
|
|
$$ LANGUAGE plpgsql VOLATILE;
|
|
|
|
-- Trigger function to set unique short ID
|
|
CREATE OR REPLACE FUNCTION public.set_calendar_short_id()
|
|
RETURNS TRIGGER AS $$
|
|
DECLARE
|
|
new_id text;
|
|
attempts integer := 0;
|
|
BEGIN
|
|
LOOP
|
|
new_id := generate_short_id();
|
|
BEGIN
|
|
NEW.short_id = new_id;
|
|
RETURN NEW;
|
|
EXCEPTION WHEN unique_violation THEN
|
|
attempts := attempts + 1;
|
|
IF attempts > 5 THEN
|
|
RAISE EXCEPTION 'Could not generate unique ID after 5 attempts';
|
|
END IF;
|
|
CONTINUE;
|
|
END;
|
|
END LOOP;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
-- Create trigger
|
|
CREATE TRIGGER set_calendar_short_id
|
|
BEFORE INSERT ON calendars
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION public.set_calendar_short_id();
|