|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
DO |
|
$$ |
|
BEGIN |
|
|
|
IF NOT EXISTS(SELECT 1 |
|
FROM pg_type t |
|
JOIN pg_namespace n ON n.oid = t.typnamespace |
|
WHERE t.typname = 'account_role' |
|
AND n.nspname = 'basejump') THEN |
|
CREATE TYPE basejump.account_role AS ENUM ('owner', 'member'); |
|
end if; |
|
end; |
|
$$; |
|
|
|
|
|
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS basejump.accounts |
|
( |
|
id uuid unique NOT NULL DEFAULT extensions.uuid_generate_v4(), |
|
|
|
|
|
|
|
primary_owner_user_id uuid references auth.users not null default auth.uid(), |
|
|
|
name text, |
|
slug text unique, |
|
personal_account boolean default false not null, |
|
updated_at timestamp with time zone, |
|
created_at timestamp with time zone, |
|
created_by uuid references auth.users, |
|
updated_by uuid references auth.users, |
|
private_metadata jsonb default '{}'::jsonb, |
|
public_metadata jsonb default '{}'::jsonb, |
|
PRIMARY KEY (id) |
|
); |
|
|
|
|
|
|
|
ALTER TABLE basejump.accounts |
|
ADD CONSTRAINT basejump_accounts_slug_null_if_personal_account_true CHECK ( |
|
(personal_account = true AND slug is null) |
|
OR (personal_account = false AND slug is not null) |
|
); |
|
|
|
|
|
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE basejump.accounts TO authenticated, service_role; |
|
|
|
|
|
|
|
|
|
|
|
|
|
CREATE OR REPLACE FUNCTION basejump.protect_account_fields() |
|
RETURNS TRIGGER AS |
|
$$ |
|
BEGIN |
|
IF current_user IN ('authenticated', 'anon') THEN |
|
|
|
|
|
if NEW.id <> OLD.id |
|
OR NEW.personal_account <> OLD.personal_account |
|
OR NEW.primary_owner_user_id <> OLD.primary_owner_user_id |
|
THEN |
|
RAISE EXCEPTION 'You do not have permission to update this field'; |
|
end if; |
|
end if; |
|
|
|
RETURN NEW; |
|
END |
|
$$ LANGUAGE plpgsql; |
|
|
|
|
|
CREATE TRIGGER basejump_protect_account_fields |
|
BEFORE UPDATE |
|
ON basejump.accounts |
|
FOR EACH ROW |
|
EXECUTE FUNCTION basejump.protect_account_fields(); |
|
|
|
|
|
CREATE OR REPLACE FUNCTION basejump.slugify_account_slug() |
|
RETURNS TRIGGER AS |
|
$$ |
|
BEGIN |
|
if NEW.slug is not null then |
|
NEW.slug = lower(regexp_replace(NEW.slug, '[^a-zA-Z0-9-]+', '-', 'g')); |
|
end if; |
|
|
|
RETURN NEW; |
|
END |
|
$$ LANGUAGE plpgsql; |
|
|
|
|
|
CREATE TRIGGER basejump_slugify_account_slug |
|
BEFORE INSERT OR UPDATE |
|
ON basejump.accounts |
|
FOR EACH ROW |
|
EXECUTE FUNCTION basejump.slugify_account_slug(); |
|
|
|
|
|
alter table basejump.accounts |
|
enable row level security; |
|
|
|
|
|
CREATE TRIGGER basejump_set_accounts_timestamp |
|
BEFORE INSERT OR UPDATE |
|
ON basejump.accounts |
|
FOR EACH ROW |
|
EXECUTE PROCEDURE basejump.trigger_set_timestamps(); |
|
|
|
|
|
CREATE TRIGGER basejump_set_accounts_user_tracking |
|
BEFORE INSERT OR UPDATE |
|
ON basejump.accounts |
|
FOR EACH ROW |
|
EXECUTE PROCEDURE basejump.trigger_set_user_tracking(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
create table if not exists basejump.account_user |
|
( |
|
|
|
user_id uuid references auth.users on delete cascade not null, |
|
|
|
account_id uuid references basejump.accounts on delete cascade not null, |
|
|
|
account_role basejump.account_role not null, |
|
constraint account_user_pkey primary key (user_id, account_id) |
|
); |
|
|
|
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE basejump.account_user TO authenticated, service_role; |
|
|
|
|
|
|
|
alter table basejump.account_user |
|
enable row level security; |
|
|
|
|
|
|
|
|
|
|
|
create or replace function basejump.add_current_user_to_new_account() |
|
returns trigger |
|
language plpgsql |
|
security definer |
|
set search_path = public |
|
as |
|
$$ |
|
begin |
|
if new.primary_owner_user_id = auth.uid() then |
|
insert into basejump.account_user (account_id, user_id, account_role) |
|
values (NEW.id, auth.uid(), 'owner'); |
|
end if; |
|
return NEW; |
|
end; |
|
$$; |
|
|
|
|
|
CREATE TRIGGER basejump_add_current_user_to_new_account |
|
AFTER INSERT |
|
ON basejump.accounts |
|
FOR EACH ROW |
|
EXECUTE FUNCTION basejump.add_current_user_to_new_account(); |
|
|
|
|
|
|
|
|
|
|
|
create or replace function basejump.run_new_user_setup() |
|
returns trigger |
|
language plpgsql |
|
security definer |
|
set search_path = public |
|
as |
|
$$ |
|
declare |
|
first_account_id uuid; |
|
generated_user_name text; |
|
begin |
|
|
|
|
|
|
|
if new.email IS NOT NULL then |
|
generated_user_name := split_part(new.email, '@', 1); |
|
end if; |
|
|
|
insert into basejump.accounts (name, primary_owner_user_id, personal_account, id) |
|
values (generated_user_name, NEW.id, true, NEW.id) |
|
returning id into first_account_id; |
|
|
|
|
|
insert into basejump.account_user (account_id, user_id, account_role) |
|
values (first_account_id, NEW.id, 'owner'); |
|
|
|
return NEW; |
|
end; |
|
$$; |
|
|
|
|
|
create trigger on_auth_user_created |
|
after insert |
|
on auth.users |
|
for each row |
|
execute procedure basejump.run_new_user_setup(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
create or replace function basejump.has_role_on_account(account_id uuid, account_role basejump.account_role default null) |
|
returns boolean |
|
language sql |
|
security definer |
|
set search_path = public |
|
as |
|
$$ |
|
select exists( |
|
select 1 |
|
from basejump.account_user wu |
|
where wu.user_id = auth.uid() |
|
and wu.account_id = has_role_on_account.account_id |
|
and ( |
|
wu.account_role = has_role_on_account.account_role |
|
or has_role_on_account.account_role is null |
|
) |
|
); |
|
$$; |
|
|
|
grant execute on function basejump.has_role_on_account(uuid, basejump.account_role) to authenticated, anon, public, service_role; |
|
|
|
|
|
|
|
|
|
|
|
|
|
create or replace function basejump.get_accounts_with_role(passed_in_role basejump.account_role default null) |
|
returns setof uuid |
|
language sql |
|
security definer |
|
set search_path = public |
|
as |
|
$$ |
|
select account_id |
|
from basejump.account_user wu |
|
where wu.user_id = auth.uid() |
|
and ( |
|
wu.account_role = passed_in_role |
|
or passed_in_role is null |
|
); |
|
$$; |
|
|
|
grant execute on function basejump.get_accounts_with_role(basejump.account_role) to authenticated; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
create policy "users can view their own account_users" on basejump.account_user |
|
for select |
|
to authenticated |
|
using ( |
|
user_id = auth.uid() |
|
); |
|
|
|
create policy "users can view their teammates" on basejump.account_user |
|
for select |
|
to authenticated |
|
using ( |
|
basejump.has_role_on_account(account_id) = true |
|
); |
|
|
|
create policy "Account users can be deleted by owners except primary account owner" on basejump.account_user |
|
for delete |
|
to authenticated |
|
using ( |
|
(basejump.has_role_on_account(account_id, 'owner') = true) |
|
AND |
|
user_id != (select primary_owner_user_id |
|
from basejump.accounts |
|
where account_id = accounts.id) |
|
); |
|
|
|
create policy "Accounts are viewable by members" on basejump.accounts |
|
for select |
|
to authenticated |
|
using ( |
|
basejump.has_role_on_account(id) = true |
|
); |
|
|
|
|
|
create policy "Accounts are viewable by primary owner" on basejump.accounts |
|
for select |
|
to authenticated |
|
using ( |
|
primary_owner_user_id = auth.uid() |
|
); |
|
|
|
create policy "Team accounts can be created by any user" on basejump.accounts |
|
for insert |
|
to authenticated |
|
with check ( |
|
basejump.is_set('enable_team_accounts') = true |
|
and personal_account = false |
|
); |
|
|
|
|
|
create policy "Accounts can be edited by owners" on basejump.accounts |
|
for update |
|
to authenticated |
|
using ( |
|
basejump.has_role_on_account(id, 'owner') = true |
|
); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
create or replace function public.get_account_id(slug text) |
|
returns uuid |
|
language sql |
|
as |
|
$$ |
|
select id |
|
from basejump.accounts |
|
where slug = get_account_id.slug; |
|
$$; |
|
|
|
grant execute on function public.get_account_id(text) to authenticated, service_role; |
|
|
|
|
|
|
|
|
|
create or replace function public.current_user_account_role(account_id uuid) |
|
returns jsonb |
|
language plpgsql |
|
as |
|
$$ |
|
DECLARE |
|
response jsonb; |
|
BEGIN |
|
|
|
select jsonb_build_object( |
|
'account_role', wu.account_role, |
|
'is_primary_owner', a.primary_owner_user_id = auth.uid(), |
|
'is_personal_account', a.personal_account |
|
) |
|
into response |
|
from basejump.account_user wu |
|
join basejump.accounts a on a.id = wu.account_id |
|
where wu.user_id = auth.uid() |
|
and wu.account_id = current_user_account_role.account_id; |
|
|
|
|
|
if response ->> 'account_role' IS NULL then |
|
raise exception 'Not found'; |
|
end if; |
|
|
|
return response; |
|
END |
|
$$; |
|
|
|
grant execute on function public.current_user_account_role(uuid) to authenticated; |
|
|
|
|
|
|
|
|
|
create or replace function public.update_account_user_role(account_id uuid, user_id uuid, |
|
new_account_role basejump.account_role, |
|
make_primary_owner boolean default false) |
|
returns void |
|
security definer |
|
set search_path = public |
|
language plpgsql |
|
as |
|
$$ |
|
declare |
|
is_account_owner boolean; |
|
is_account_primary_owner boolean; |
|
changing_primary_owner boolean; |
|
begin |
|
|
|
select basejump.has_role_on_account(update_account_user_role.account_id, 'owner') into is_account_owner; |
|
|
|
if not is_account_owner then |
|
raise exception 'You must be an owner of the account to update a users role'; |
|
end if; |
|
|
|
|
|
select primary_owner_user_id = auth.uid(), primary_owner_user_id = update_account_user_role.user_id |
|
into is_account_primary_owner, changing_primary_owner |
|
from basejump.accounts |
|
where id = update_account_user_role.account_id; |
|
|
|
if changing_primary_owner = true and is_account_primary_owner = false then |
|
raise exception 'You must be the primary owner of the account to change the primary owner'; |
|
end if; |
|
|
|
update basejump.account_user au |
|
set account_role = new_account_role |
|
where au.account_id = update_account_user_role.account_id |
|
and au.user_id = update_account_user_role.user_id; |
|
|
|
if make_primary_owner = true then |
|
|
|
if is_account_primary_owner = false then |
|
raise exception 'You must be the primary owner of the account to change the primary owner'; |
|
end if; |
|
|
|
update basejump.accounts |
|
set primary_owner_user_id = update_account_user_role.user_id |
|
where id = update_account_user_role.account_id; |
|
end if; |
|
end; |
|
$$; |
|
|
|
grant execute on function public.update_account_user_role(uuid, uuid, basejump.account_role, boolean) to authenticated; |
|
|
|
|
|
|
|
|
|
create or replace function public.get_accounts() |
|
returns json |
|
language sql |
|
as |
|
$$ |
|
select coalesce(json_agg( |
|
json_build_object( |
|
'account_id', wu.account_id, |
|
'account_role', wu.account_role, |
|
'is_primary_owner', a.primary_owner_user_id = auth.uid(), |
|
'name', a.name, |
|
'slug', a.slug, |
|
'personal_account', a.personal_account, |
|
'created_at', a.created_at, |
|
'updated_at', a.updated_at |
|
) |
|
), '[]'::json) |
|
from basejump.account_user wu |
|
join basejump.accounts a on a.id = wu.account_id |
|
where wu.user_id = auth.uid(); |
|
$$; |
|
|
|
grant execute on function public.get_accounts() to authenticated; |
|
|
|
|
|
|
|
|
|
create or replace function public.get_account(account_id uuid) |
|
returns json |
|
language plpgsql |
|
as |
|
$$ |
|
BEGIN |
|
|
|
if current_user IN ('anon', 'authenticated') and |
|
(select current_user_account_role(get_account.account_id) ->> 'account_role' IS NULL) then |
|
raise exception 'You must be a member of an account to access it'; |
|
end if; |
|
|
|
|
|
return (select json_build_object( |
|
'account_id', a.id, |
|
'account_role', wu.account_role, |
|
'is_primary_owner', a.primary_owner_user_id = auth.uid(), |
|
'name', a.name, |
|
'slug', a.slug, |
|
'personal_account', a.personal_account, |
|
'billing_enabled', case |
|
when a.personal_account = true then |
|
config.enable_personal_account_billing |
|
else |
|
config.enable_team_account_billing |
|
end, |
|
'billing_status', bs.status, |
|
'created_at', a.created_at, |
|
'updated_at', a.updated_at, |
|
'metadata', a.public_metadata |
|
) |
|
from basejump.accounts a |
|
left join basejump.account_user wu on a.id = wu.account_id and wu.user_id = auth.uid() |
|
join basejump.config config on true |
|
left join (select bs.account_id, status |
|
from basejump.billing_subscriptions bs |
|
where bs.account_id = get_account.account_id |
|
order by created desc |
|
limit 1) bs on bs.account_id = a.id |
|
where a.id = get_account.account_id); |
|
END; |
|
$$; |
|
|
|
grant execute on function public.get_account(uuid) to authenticated, service_role; |
|
|
|
|
|
|
|
|
|
create or replace function public.get_account_by_slug(slug text) |
|
returns json |
|
language plpgsql |
|
as |
|
$$ |
|
DECLARE |
|
internal_account_id uuid; |
|
BEGIN |
|
select a.id |
|
into internal_account_id |
|
from basejump.accounts a |
|
where a.slug IS NOT NULL |
|
and a.slug = get_account_by_slug.slug; |
|
|
|
return public.get_account(internal_account_id); |
|
END; |
|
$$; |
|
|
|
grant execute on function public.get_account_by_slug(text) to authenticated; |
|
|
|
|
|
|
|
|
|
create or replace function public.get_personal_account() |
|
returns json |
|
language plpgsql |
|
as |
|
$$ |
|
BEGIN |
|
return public.get_account(auth.uid()); |
|
END; |
|
$$; |
|
|
|
grant execute on function public.get_personal_account() to authenticated; |
|
|
|
|
|
|
|
|
|
create or replace function public.create_account(slug text default null, name text default null) |
|
returns json |
|
language plpgsql |
|
as |
|
$$ |
|
DECLARE |
|
new_account_id uuid; |
|
BEGIN |
|
insert into basejump.accounts (slug, name) |
|
values (create_account.slug, create_account.name) |
|
returning id into new_account_id; |
|
|
|
return public.get_account(new_account_id); |
|
EXCEPTION |
|
WHEN unique_violation THEN |
|
raise exception 'An account with that unique ID already exists'; |
|
END; |
|
$$; |
|
|
|
grant execute on function public.create_account(slug text, name text) to authenticated; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
create or replace function public.update_account(account_id uuid, slug text default null, name text default null, |
|
public_metadata jsonb default null, |
|
replace_metadata boolean default false) |
|
returns json |
|
language plpgsql |
|
as |
|
$$ |
|
BEGIN |
|
|
|
|
|
if current_user IN ('anon', 'authenticated') and |
|
not (select current_user_account_role(update_account.account_id) ->> 'account_role' = 'owner') then |
|
raise exception 'Only account owners can update an account'; |
|
end if; |
|
|
|
update basejump.accounts accounts |
|
set slug = coalesce(update_account.slug, accounts.slug), |
|
name = coalesce(update_account.name, accounts.name), |
|
public_metadata = case |
|
when update_account.public_metadata is null then accounts.public_metadata |
|
when accounts.public_metadata IS NULL then update_account.public_metadata |
|
when update_account.replace_metadata |
|
then update_account.public_metadata |
|
else accounts.public_metadata || update_account.public_metadata end |
|
where accounts.id = update_account.account_id; |
|
|
|
return public.get_account(account_id); |
|
END; |
|
$$; |
|
|
|
grant execute on function public.update_account(uuid, text, text, jsonb, boolean) to authenticated, service_role; |
|
|
|
|
|
|
|
|
|
|
|
|
|
create or replace function public.get_account_members(account_id uuid, results_limit integer default 50, |
|
results_offset integer default 0) |
|
returns json |
|
language plpgsql |
|
security definer |
|
set search_path = basejump |
|
as |
|
$$ |
|
BEGIN |
|
|
|
|
|
if (select public.current_user_account_role(get_account_members.account_id) ->> 'account_role' <> 'owner') then |
|
raise exception 'Only account owners can access this function'; |
|
end if; |
|
|
|
return (select json_agg( |
|
json_build_object( |
|
'user_id', wu.user_id, |
|
'account_role', wu.account_role, |
|
'name', p.name, |
|
'email', u.email, |
|
'is_primary_owner', a.primary_owner_user_id = wu.user_id |
|
) |
|
) |
|
from basejump.account_user wu |
|
join basejump.accounts a on a.id = wu.account_id |
|
join basejump.accounts p on p.primary_owner_user_id = wu.user_id and p.personal_account = true |
|
join auth.users u on u.id = wu.user_id |
|
where wu.account_id = get_account_members.account_id |
|
limit coalesce(get_account_members.results_limit, 50) offset coalesce(get_account_members.results_offset, 0)); |
|
END; |
|
$$; |
|
|
|
grant execute on function public.get_account_members(uuid, integer, integer) to authenticated; |
|
|
|
|
|
|
|
|
|
|
|
create or replace function public.remove_account_member(account_id uuid, user_id uuid) |
|
returns void |
|
language plpgsql |
|
as |
|
$$ |
|
BEGIN |
|
|
|
if basejump.has_role_on_account(remove_account_member.account_id, 'owner') <> true then |
|
raise exception 'Only account owners can access this function'; |
|
end if; |
|
|
|
delete |
|
from basejump.account_user wu |
|
where wu.account_id = remove_account_member.account_id |
|
and wu.user_id = remove_account_member.user_id; |
|
END; |
|
$$; |
|
|
|
grant execute on function public.remove_account_member(uuid, uuid) to authenticated; |