- Flutter app (Android/iOS): home, explore, albums, search, player with equalizer, mini player, sign up/sign in with email codes, profile, encrypted offline downloads, About page - server/: one-command deploy of self-hosted Supabase behind HestiaCP/nginx (or Caddy), migrations, backups, email templates - supabase/migrations: schema, row-level security, profiles - tools/: song scanner and importer Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
41 lines
1.9 KiB
PL/PgSQL
41 lines
1.9 KiB
PL/PgSQL
-- Profile page: more optional details, the name given at sign-up, photos, and account deletion.
|
|
|
|
alter table profiles
|
|
add column phone text,
|
|
add column hatti_name text, -- village / hatti, free text
|
|
add column avatar_url text,
|
|
add column updated_at timestamptz not null default now();
|
|
|
|
-- Use the name typed at sign-up (user metadata), falling back to the email's first part.
|
|
create or replace function handle_new_user() returns trigger
|
|
language plpgsql security definer set search_path = public as $$
|
|
begin
|
|
insert into profiles (id, display_name)
|
|
values (new.id, coalesce(nullif(trim(new.raw_user_meta_data ->> 'display_name'), ''), split_part(new.email, '@', 1)));
|
|
return new;
|
|
end $$;
|
|
|
|
-- App stores require in-app account deletion. Removes the login; profile, likes,
|
|
-- playlists and history go with it (on delete cascade).
|
|
create function delete_my_account() returns void
|
|
language plpgsql security definer set search_path = public as $$
|
|
begin
|
|
if auth.uid() is null then
|
|
raise exception 'not signed in';
|
|
end if;
|
|
delete from storage.objects where bucket_id = 'avatars' and (storage.foldername(name))[1] = auth.uid()::text;
|
|
delete from auth.users where id = auth.uid();
|
|
end $$;
|
|
|
|
revoke all on function delete_my_account() from public, anon;
|
|
grant execute on function delete_my_account() to authenticated;
|
|
|
|
-- Profile photos: avatars/<user id>/<file>. Public to read, each user writes only their own folder.
|
|
insert into storage.buckets (id, name, public, file_size_limit, allowed_mime_types)
|
|
values ('avatars', 'avatars', true, 2097152, array['image/jpeg','image/png','image/webp'])
|
|
on conflict (id) do nothing;
|
|
|
|
create policy nk_own_avatar on storage.objects for all to authenticated
|
|
using (bucket_id = 'avatars' and (storage.foldername(name))[1] = auth.uid()::text)
|
|
with check (bucket_id = 'avatars' and (storage.foldername(name))[1] = auth.uid()::text);
|