init
This commit is contained in:
commit
5aa9b32c03
27 changed files with 4142 additions and 0 deletions
4
supabase/.gitignore
vendored
Normal file
4
supabase/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# Supabase
|
||||
.branches
|
||||
.temp
|
||||
.env
|
||||
88
supabase/config.toml
Normal file
88
supabase/config.toml
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
# A string used to distinguish different Supabase projects on the same host. Defaults to the
|
||||
# working directory name when running `supabase init`.
|
||||
project_id = "slack-clone"
|
||||
|
||||
[api]
|
||||
enabled = true
|
||||
# Port to use for the API URL.
|
||||
port = 54321
|
||||
# Schemas to expose in your API. Tables, views and stored procedures in this schema will get API
|
||||
# endpoints. public and storage are always included.
|
||||
schemas = ["public"]
|
||||
# Extra schemas to add to the search_path of every request. public is always included.
|
||||
extra_search_path = ["public", "extensions"]
|
||||
# The maximum number of rows returns from a view, table, or stored procedure. Limits payload size
|
||||
# for accidental or malicious requests.
|
||||
max_rows = 1000
|
||||
|
||||
[db]
|
||||
# Port to use for the local database URL.
|
||||
port = 54322
|
||||
# Port used by db diff command to initialize the shadow database.
|
||||
shadow_port = 54320
|
||||
# The database major version to use. This has to be the same as your remote database's. Run `SHOW
|
||||
# server_version;` on the remote database to check.
|
||||
major_version = 15
|
||||
|
||||
[realtime]
|
||||
enabled = true
|
||||
# Bind realtime via either IPv4 or IPv6. (default: IPv6)
|
||||
# ip_version = "IPv6"
|
||||
|
||||
[auth]
|
||||
enabled = true
|
||||
# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used
|
||||
# in emails.
|
||||
site_url = "env(NEXT_SITE_URL)"
|
||||
# A list of *exact* URLs that auth providers are permitted to redirect to post authentication.
|
||||
additional_redirect_urls = [
|
||||
# Will be localhost:3000 in development or the URL of your deployed app in production.
|
||||
"env(NEXT_REDIRECT_URLS)",
|
||||
]
|
||||
# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week).
|
||||
jwt_expiry = 3600
|
||||
# If disabled, the refresh token will never expire.
|
||||
enable_refresh_token_rotation = true
|
||||
# Allows refresh tokens to be reused after expiry, up to the specified interval in seconds.
|
||||
# Requires enable_refresh_token_rotation = true.
|
||||
refresh_token_reuse_interval = 10
|
||||
# Allow/disallow new user signups to your project.
|
||||
enable_signup = true
|
||||
|
||||
[auth.email]
|
||||
# Allow/disallow new user signups via email to your project.
|
||||
enable_signup = true
|
||||
# If enabled, a user will be required to confirm any email change on both the old, and new email
|
||||
# addresses. If disabled, only the new email is required to confirm.
|
||||
double_confirm_changes = true
|
||||
# If enabled, users need to confirm their email address before signing in.
|
||||
enable_confirmations = true
|
||||
# If enabled, users will need to reauthenticate or have logged in recently to change their password.
|
||||
secure_password_change = false
|
||||
# Controls the minimum amount of time that must pass before sending another signup confirmation or password reset email.
|
||||
max_frequency = "1m0s"
|
||||
# Number of characters used in the email OTP.
|
||||
otp_length = 6
|
||||
# Number of seconds before the email OTP expires (defaults to 1 hour).
|
||||
otp_expiry = 3600
|
||||
|
||||
# Email testing server. Emails sent with the local dev setup are not actually sent - rather, they
|
||||
# are monitored, and you can view the emails that would have been sent from the web interface.
|
||||
[inbucket]
|
||||
enabled = true
|
||||
# Port to use for the email testing server web interface.
|
||||
port = 54324
|
||||
# Uncomment to expose additional ports for testing user applications that send emails.
|
||||
# smtp_port = 54325
|
||||
# pop3_port = 54326
|
||||
# admin_email = "admin@email.com"
|
||||
# sender_name = "Admin"
|
||||
|
||||
# Enable auth hooks
|
||||
# https://supabase.com/docs/guides/auth/auth-hooks#local-development
|
||||
[auth.hook.custom_access_token]
|
||||
enabled = true
|
||||
uri = "pg-functions://postgres/public/custom_access_token_hook"
|
||||
|
||||
[analytics]
|
||||
enabled = false
|
||||
164
supabase/migrations/20240214102356_init.sql
Normal file
164
supabase/migrations/20240214102356_init.sql
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
--
|
||||
-- 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 ('channels.delete', 'messages.delete');
|
||||
create type public.app_role as enum ('admin', 'moderator');
|
||||
create type public.user_status as enum ('ONLINE', 'OFFLINE');
|
||||
|
||||
-- USERS
|
||||
create table public.users (
|
||||
id uuid references auth.users not null primary key, -- UUID from auth.users
|
||||
username text,
|
||||
status user_status default 'OFFLINE'::public.user_status
|
||||
);
|
||||
comment on table public.users is 'Profile data for each user.';
|
||||
comment on column public.users.id is 'References the internal Supabase Auth user.';
|
||||
|
||||
-- CHANNELS
|
||||
create table public.channels (
|
||||
id bigint generated by default as identity primary key,
|
||||
inserted_at timestamp with time zone default timezone('utc'::text, now()) not null,
|
||||
slug text not null unique,
|
||||
created_by uuid references public.users not null
|
||||
);
|
||||
comment on table public.channels is 'Topics and groups.';
|
||||
|
||||
-- MESSAGES
|
||||
create table public.messages (
|
||||
id bigint generated by default as identity primary key,
|
||||
inserted_at timestamp with time zone default timezone('utc'::text, now()) not null,
|
||||
message text,
|
||||
user_id uuid references public.users not null,
|
||||
channel_id bigint references public.channels on delete cascade not null
|
||||
);
|
||||
comment on table public.messages is 'Individual messages sent by each user.';
|
||||
|
||||
-- USER ROLES
|
||||
create table public.user_roles (
|
||||
id bigint generated by default as identity primary key,
|
||||
user_id uuid references public.users on delete cascade not null,
|
||||
role app_role not null,
|
||||
unique (user_id, role)
|
||||
);
|
||||
comment on table public.user_roles is 'Application roles for each user.';
|
||||
|
||||
-- ROLE 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.';
|
||||
|
||||
-- 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.channels enable row level security;
|
||||
alter table public.messages enable row level security;
|
||||
alter table public.user_roles enable row level security;
|
||||
alter table public.role_permissions enable row level security;
|
||||
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 logged-in read access" on public.channels for select using ( auth.role() = 'authenticated' );
|
||||
create policy "Allow individual insert access" on public.channels for insert with check ( auth.uid() = created_by );
|
||||
create policy "Allow individual delete access" on public.channels for delete using ( auth.uid() = created_by );
|
||||
create policy "Allow authorized delete access" on public.channels for delete using ( authorize('channels.delete') );
|
||||
create policy "Allow logged-in read access" on public.messages for select using ( auth.role() = 'authenticated' );
|
||||
create policy "Allow individual insert access" on public.messages for insert with check ( auth.uid() = user_id );
|
||||
create policy "Allow individual update access" on public.messages for update using ( auth.uid() = user_id );
|
||||
create policy "Allow individual delete access" on public.messages for delete using ( auth.uid() = user_id );
|
||||
create policy "Allow authorized delete access" on public.messages for delete using ( authorize('messages.delete') );
|
||||
create policy "Allow individual read access" on public.user_roles for select using ( auth.uid() = user_id );
|
||||
|
||||
-- Send "previous data" on change
|
||||
alter table public.users replica identity full;
|
||||
alter table public.channels replica identity full;
|
||||
alter table public.messages 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);
|
||||
|
||||
select count(*) = 1 from auth.users into is_admin;
|
||||
|
||||
if position('+supaadmin@' in new.email) > 0 then
|
||||
insert into public.user_roles (user_id, role) values (new.id, 'admin');
|
||||
elsif position('+supamod@' in new.email) > 0 then
|
||||
insert into public.user_roles (user_id, role) values (new.id, 'moderator');
|
||||
end if;
|
||||
|
||||
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();
|
||||
|
||||
/**
|
||||
* REALTIME SUBSCRIPTIONS
|
||||
* Only allow realtime listening on public tables.
|
||||
*/
|
||||
|
||||
begin;
|
||||
-- remove the realtime publication
|
||||
drop publication if exists supabase_realtime;
|
||||
|
||||
-- re-create the publication but don't enable it for any tables
|
||||
create publication supabase_realtime;
|
||||
commit;
|
||||
|
||||
-- add tables to the publication
|
||||
alter publication supabase_realtime add table public.channels;
|
||||
alter publication supabase_realtime add table public.messages;
|
||||
alter publication supabase_realtime add table public.users;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
58
supabase/migrations/20240214114147_auth-hook.sql
Normal file
58
supabase/migrations/20240214114147_auth-hook.sql
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
/**
|
||||
* AUTH HOOKS
|
||||
* Create an auth hook to add a custom claim to the access token jwt.
|
||||
*/
|
||||
|
||||
-- Create the auth hook function
|
||||
-- https://supabase.com/docs/guides/auth/auth-hooks#hook-custom-access-token
|
||||
create or replace function public.custom_access_token_hook(event jsonb)
|
||||
returns jsonb
|
||||
language plpgsql
|
||||
stable
|
||||
as $$
|
||||
declare
|
||||
claims jsonb;
|
||||
user_role public.app_role;
|
||||
begin
|
||||
-- Check if the user is marked as admin in the profiles table
|
||||
select role into user_role from public.user_roles where user_id = (event->>'user_id')::uuid;
|
||||
|
||||
claims := event->'claims';
|
||||
|
||||
if user_role is not null then
|
||||
-- Set the claim
|
||||
claims := jsonb_set(claims, '{user_role}', to_jsonb(user_role));
|
||||
else
|
||||
claims := jsonb_set(claims, '{user_role}', 'null');
|
||||
end if;
|
||||
|
||||
-- Update the 'claims' object in the original event
|
||||
event := jsonb_set(event, '{claims}', claims);
|
||||
|
||||
-- Return the modified or original event
|
||||
return event;
|
||||
end;
|
||||
$$;
|
||||
|
||||
grant usage on schema public to supabase_auth_admin;
|
||||
|
||||
grant execute
|
||||
on function public.custom_access_token_hook
|
||||
to supabase_auth_admin;
|
||||
|
||||
revoke execute
|
||||
on function public.custom_access_token_hook
|
||||
from authenticated, anon;
|
||||
|
||||
grant all
|
||||
on table public.user_roles
|
||||
to supabase_auth_admin;
|
||||
|
||||
revoke all
|
||||
on table public.user_roles
|
||||
from authenticated, anon;
|
||||
|
||||
create policy "Allow auth admin to read user roles" ON public.user_roles
|
||||
as permissive for select
|
||||
to supabase_auth_admin
|
||||
using (true)
|
||||
23
supabase/seed.sql
Normal file
23
supabase/seed.sql
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
insert into public.role_permissions (role, permission)
|
||||
values
|
||||
('admin', 'channels.delete'),
|
||||
('admin', 'messages.delete'),
|
||||
('moderator', 'messages.delete');
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
user_id uuid;
|
||||
BEGIN
|
||||
user_id := public.create_user('supabot+supaadmin@example.com');
|
||||
|
||||
insert into public.channels (slug, created_by)
|
||||
values
|
||||
('public', user_id),
|
||||
('random', user_id);
|
||||
|
||||
insert into public.messages (message, channel_id, user_id)
|
||||
values
|
||||
('Hello World 👋', 1, user_id),
|
||||
('Perfection is attained, not when there is nothing more to add, but when there is nothing left to take away.', 2, user_id);
|
||||
END $$;
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue