Initial commit
This commit is contained in:
@@ -0,0 +1,278 @@
|
||||
-- ============================================================
|
||||
-- 0001_init.sql — Devodytyji schema, RLS, storage, seed helpers
|
||||
-- Run in the Supabase SQL editor or via `supabase db push`.
|
||||
-- ============================================================
|
||||
|
||||
-- ---------- profiles ----------
|
||||
create table public.profiles (
|
||||
id uuid primary key references auth.users (id) on delete cascade,
|
||||
full_name text,
|
||||
avatar_url text,
|
||||
is_admin boolean not null default false,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
-- Auto-create a profile for every new auth user
|
||||
create or replace function public.handle_new_user()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
security definer
|
||||
set search_path = public
|
||||
as $$
|
||||
begin
|
||||
insert into public.profiles (id, full_name, avatar_url)
|
||||
values (
|
||||
new.id,
|
||||
coalesce(
|
||||
new.raw_user_meta_data->>'full_name',
|
||||
new.raw_user_meta_data->>'name',
|
||||
split_part(coalesce(new.email, ''), '@', 1)
|
||||
),
|
||||
new.raw_user_meta_data->>'avatar_url'
|
||||
)
|
||||
on conflict (id) do nothing;
|
||||
return new;
|
||||
end;
|
||||
$$;
|
||||
|
||||
create trigger on_auth_user_created
|
||||
after insert on auth.users
|
||||
for each row execute function public.handle_new_user();
|
||||
|
||||
-- Helper to promote a user to admin by email (run from dashboard/psql)
|
||||
create or replace function public.promote_user_to_admin(target_email text)
|
||||
returns void
|
||||
language plpgsql
|
||||
security definer
|
||||
set search_path = public
|
||||
as $$
|
||||
begin
|
||||
update public.profiles p
|
||||
set is_admin = true
|
||||
from auth.users u
|
||||
where u.id = p.id
|
||||
and lower(u.email) = lower(target_email);
|
||||
if not found then
|
||||
raise exception 'No user found with email %', target_email;
|
||||
end if;
|
||||
end;
|
||||
$$;
|
||||
|
||||
-- ---------- events ----------
|
||||
create table public.events (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
slug text unique not null,
|
||||
title jsonb not null,
|
||||
description jsonb not null default '{"ua":"","en":""}'::jsonb,
|
||||
cover_image_url text,
|
||||
starts_at timestamptz not null,
|
||||
ends_at timestamptz not null,
|
||||
venue_name text,
|
||||
status text not null default 'draft' check (status in ('draft', 'published')),
|
||||
created_by uuid references public.profiles (id) on delete set null,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
create index events_slug_idx on public.events (slug);
|
||||
create index events_starts_at_idx on public.events (starts_at);
|
||||
create index events_status_idx on public.events (status);
|
||||
|
||||
-- ---------- event_reviews ----------
|
||||
create table public.event_reviews (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
event_id uuid not null references public.events (id) on delete cascade,
|
||||
user_id uuid not null references public.profiles (id) on delete cascade,
|
||||
rating int not null check (rating between 1 and 5),
|
||||
comment text,
|
||||
created_at timestamptz not null default now(),
|
||||
unique (event_id, user_id)
|
||||
);
|
||||
|
||||
create index event_reviews_event_id_idx on public.event_reviews (event_id);
|
||||
|
||||
-- ---------- venue_reviews ----------
|
||||
create table public.venue_reviews (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
user_id uuid not null references public.profiles (id) on delete cascade,
|
||||
rating int not null check (rating between 1 and 5),
|
||||
comment text,
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
-- ---------- contact_messages ----------
|
||||
create table public.contact_messages (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
name text not null,
|
||||
email text not null,
|
||||
message text not null,
|
||||
read boolean not null default false,
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
create index contact_messages_read_idx on public.contact_messages (read);
|
||||
|
||||
-- ---------- updated_at trigger ----------
|
||||
create or replace function public.set_updated_at()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
as $$
|
||||
begin
|
||||
new.updated_at = now();
|
||||
return new;
|
||||
end;
|
||||
$$;
|
||||
|
||||
create trigger profiles_set_updated_at
|
||||
before update on public.profiles
|
||||
for each row execute function public.set_updated_at();
|
||||
|
||||
create trigger events_set_updated_at
|
||||
before update on public.events
|
||||
for each row execute function public.set_updated_at();
|
||||
|
||||
-- ---------- contact rate-limit helper (security definer so anon can use it) ----------
|
||||
create or replace function public.recent_contact_messages(p_email text, p_within interval default interval '10 minutes')
|
||||
returns bigint
|
||||
language sql
|
||||
security definer
|
||||
set search_path = public
|
||||
stable
|
||||
as $$
|
||||
select count(*)::bigint
|
||||
from public.contact_messages
|
||||
where email = p_email
|
||||
and created_at > now() - p_within;
|
||||
$$;
|
||||
|
||||
grant execute on function public.recent_contact_messages(text, interval) to anon, authenticated;
|
||||
|
||||
-- ============================================================
|
||||
-- Row Level Security
|
||||
-- ============================================================
|
||||
|
||||
alter table public.profiles enable row level security;
|
||||
alter table public.events enable row level security;
|
||||
alter table public.event_reviews enable row level security;
|
||||
alter table public.venue_reviews enable row level security;
|
||||
alter table public.contact_messages enable row level security;
|
||||
|
||||
-- profiles: public read, self edit
|
||||
create policy "profiles_select_public" on public.profiles
|
||||
for select using (true);
|
||||
|
||||
create policy "profiles_update_self" on public.profiles
|
||||
for update using (auth.uid() = id);
|
||||
|
||||
-- events: published visible to all; admins full CRUD
|
||||
create policy "events_select_published" on public.events
|
||||
for select using (status = 'published');
|
||||
|
||||
create policy "events_select_admin" on public.events
|
||||
for select using (
|
||||
exists (select 1 from public.profiles p where p.id = auth.uid() and p.is_admin)
|
||||
);
|
||||
|
||||
create policy "events_insert_admin" on public.events
|
||||
for insert with check (
|
||||
exists (select 1 from public.profiles p where p.id = auth.uid() and p.is_admin)
|
||||
);
|
||||
|
||||
create policy "events_update_admin" on public.events
|
||||
for update using (
|
||||
exists (select 1 from public.profiles p where p.id = auth.uid() and p.is_admin)
|
||||
);
|
||||
|
||||
create policy "events_delete_admin" on public.events
|
||||
for delete using (
|
||||
exists (select 1 from public.profiles p where p.id = auth.uid() and p.is_admin)
|
||||
);
|
||||
|
||||
-- event_reviews: public read, authenticated insert, owner update/delete, admin delete
|
||||
create policy "event_reviews_select_public" on public.event_reviews
|
||||
for select using (true);
|
||||
|
||||
create policy "event_reviews_insert_auth" on public.event_reviews
|
||||
for insert with check (auth.uid() = user_id);
|
||||
|
||||
create policy "event_reviews_update_owner" on public.event_reviews
|
||||
for update using (auth.uid() = user_id);
|
||||
|
||||
create policy "event_reviews_delete_owner" on public.event_reviews
|
||||
for delete using (auth.uid() = user_id);
|
||||
|
||||
create policy "event_reviews_delete_admin" on public.event_reviews
|
||||
for delete using (
|
||||
exists (select 1 from public.profiles p where p.id = auth.uid() and p.is_admin)
|
||||
);
|
||||
|
||||
-- venue_reviews: public read, authenticated insert, owner update/delete, admin delete
|
||||
create policy "venue_reviews_select_public" on public.venue_reviews
|
||||
for select using (true);
|
||||
|
||||
create policy "venue_reviews_insert_auth" on public.venue_reviews
|
||||
for insert with check (auth.uid() = user_id);
|
||||
|
||||
create policy "venue_reviews_update_owner" on public.venue_reviews
|
||||
for update using (auth.uid() = user_id);
|
||||
|
||||
create policy "venue_reviews_delete_owner" on public.venue_reviews
|
||||
for delete using (auth.uid() = user_id);
|
||||
|
||||
create policy "venue_reviews_delete_admin" on public.venue_reviews
|
||||
for delete using (
|
||||
exists (select 1 from public.profiles p where p.id = auth.uid() and p.is_admin)
|
||||
);
|
||||
|
||||
-- contact_messages: anon insert, admin read/update/delete
|
||||
create policy "contact_messages_insert_anon" on public.contact_messages
|
||||
for insert with check (true);
|
||||
|
||||
create policy "contact_messages_select_admin" on public.contact_messages
|
||||
for select using (
|
||||
exists (select 1 from public.profiles p where p.id = auth.uid() and p.is_admin)
|
||||
);
|
||||
|
||||
create policy "contact_messages_update_admin" on public.contact_messages
|
||||
for update using (
|
||||
exists (select 1 from public.profiles p where p.id = auth.uid() and p.is_admin)
|
||||
);
|
||||
|
||||
create policy "contact_messages_delete_admin" on public.contact_messages
|
||||
for delete using (
|
||||
exists (select 1 from public.profiles p where p.id = auth.uid() and p.is_admin)
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
-- Storage: event-covers bucket (public read, admin write)
|
||||
-- ============================================================
|
||||
insert into storage.buckets (id, name, public)
|
||||
values ('event-covers', 'event-covers', true)
|
||||
on conflict (id) do nothing;
|
||||
|
||||
create policy "event_covers_select_public" on storage.objects
|
||||
for select using (bucket_id = 'event-covers');
|
||||
|
||||
create policy "event_covers_insert_admin" on storage.objects
|
||||
for insert with check (
|
||||
bucket_id = 'event-covers'
|
||||
and exists (select 1 from public.profiles p where p.id = auth.uid() and p.is_admin)
|
||||
);
|
||||
|
||||
create policy "event_covers_update_admin" on storage.objects
|
||||
for update using (
|
||||
bucket_id = 'event-covers'
|
||||
and exists (select 1 from public.profiles p where p.id = auth.uid() and p.is_admin)
|
||||
);
|
||||
|
||||
create policy "event_covers_delete_admin" on storage.objects
|
||||
for delete using (
|
||||
bucket_id = 'event-covers'
|
||||
and exists (select 1 from public.profiles p where p.id = auth.uid() and p.is_admin)
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
-- Auth providers (Google + GitHub) must be enabled in the
|
||||
-- Supabase dashboard: Authentication → Providers → Google/GitHub.
|
||||
-- ============================================================
|
||||
Reference in New Issue
Block a user