Initial commit

This commit is contained in:
ArtemBozhko
2026-08-14 08:51:55 +03:00
commit 957f1d4ba2
81 changed files with 19444 additions and 0 deletions
+278
View File
@@ -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.
-- ============================================================
+59
View File
@@ -0,0 +1,59 @@
-- ============================================================
-- seed.sql — sample events and demo messages.
-- Run AFTER 0001_init.sql.
--
-- Promote yourself to admin (replace with your email):
-- select public.promote_user_to_admin('you@example.com');
-- ============================================================
insert into public.events (slug, title, description, starts_at, ends_at, venue_name, status) values
(
'open-air-cinema-under-the-stars',
'{"ua": "Кіно просто неба: «Тіні забутих предків»", "en": "Open-air cinema: Shadows of Forgotten Ancestors"}',
'{"ua": "Культова стрічка на великому екрані під зоряним небом. Беріть плед, гарячий чай і друзів — початок після заходу сонця.", "en": "A cult film on the big screen under the starry sky. Bring a blanket, hot tea and friends — we start after sunset."}',
now() + interval '3 days',
now() + interval '3 days' + interval '2 hours',
'Devodytyji',
'published'
),
(
'summer-evening-acoustic-concert',
'{"ua": "Акустичний вечір: українські виконавці", "en": "Acoustic evening: Ukrainian artists"}',
'{"ua": "Теплий літній вечір, живий звук і кращі українські інді-гурти. Вхід вільний, обовʼязковий гарний настрій.", "en": "A warm summer evening, live sound and the best Ukrainian indie bands. Free entry, good mood mandatory."}',
now() + interval '10 days',
now() + interval '10 days' + interval '3 hours',
'Devodytyji',
'published'
),
(
'feria-of-crafts-and-food',
'{"ua": "Ярмарок крафту та їжі", "en": "Crafts and food fair"}',
'{"ua": "Десятки локальних майстрів, фермерські продукти, жива музика та дитяча зона. Ідеальний день для всієї родини.", "en": "Dozens of local makers, farm produce, live music and a kids area. A perfect day for the whole family."}',
now() + interval '21 days',
now() + interval '21 days' + interval '6 hours',
'Devodytyji',
'published'
),
(
'night-lecture-on-astronomy',
'{"ua": "Нічна лекція з астрономії", "en": "Night lecture on astronomy"}',
'{"ua": "Дивимось у небо і розбираємось, що там відбувається. Телескопи — наші, запитання — ваші.", "en": "We look at the sky and figure out what is going on up there. Telescopes are ours, questions are yours."}',
now() + interval '6 days',
now() + interval '6 days' + interval '2 hours',
'Devodytyji',
'draft'
);
insert into public.contact_messages (name, email, message, read) values
(
'Оксана Мельник',
'oksana@example.com',
'Добрий день! Чи можна прийти з собакою на кіновечір? І чи є теплі пледи?',
false
),
(
'Ivan Petrenko',
'ivan@example.com',
'Hi! Do you host private events / birthday parties? What are the rates for a Saturday?',
true
);