-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathsupabase-setup.sql
More file actions
244 lines (213 loc) · 9.75 KB
/
Copy pathsupabase-setup.sql
File metadata and controls
244 lines (213 loc) · 9.75 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
-- =============================================================
-- PayRadar — TEK SEFERDE KURULUM
-- Supabase Dashboard > SQL Editor > New query > yapıştır > Run
-- Tekrar çalıştırmak güvenlidir (idempotent).
-- =============================================================
-- Deneme/tanıtım kötüye kullanım koruması ve e-posta özeti (hash) için gerekli
create extension if not exists pgcrypto;
-- -------------------------------------------------------------
-- 1) Kasa (bulut senkronizasyonu)
-- İçerik cihazda AES-256-GCM ile şifrelenip yazılır. NOT: Anahtar,
-- hesabın UID değerinden türetildiği ve UID bu tablonun id sütununda
-- yer aldığı için bu, "sunucunun asla çözemeyeceği" uçtan uca şifreleme
-- DEĞİLDİR; tabloyu okuyabilen bir taraf anahtarı türetebilir.
-- Koruduğu durum: Yalnızca şifreli bloğu (satırın kendisini değil)
-- ele geçiren taraflar ve kazara günlüğe kaydedilen (loglanan) veya dökümü alınan içeriktir.
-- -------------------------------------------------------------
create table if not exists public.vaults (
id uuid primary key references auth.users on delete cascade,
data jsonb not null default '{}'::jsonb,
updated_at timestamptz not null default now()
);
-- Tek satır megabaytlarca büyüyemesin (RLS satırı sahibine kilitler ancak boyutu sınırlandırmaz)
alter table public.vaults drop constraint if exists vaults_data_size;
alter table public.vaults
add constraint vaults_data_size check (pg_column_size(data) < 1048576);
-- id için varsayılan değer atanmazsa istemcinin upsert işlemi NOT NULL hatası verir
alter table public.vaults alter column id set default auth.uid();
alter table public.vaults enable row level security;
-- Kullanıcı SADECE kendi satırını görebilir ve güncelleyebilir
drop policy if exists "own vault select" on public.vaults;
create policy "own vault select" on public.vaults
for select using (auth.uid() = id);
drop policy if exists "own vault insert" on public.vaults;
create policy "own vault insert" on public.vaults
for insert with check (auth.uid() = id);
drop policy if exists "own vault update" on public.vaults;
create policy "own vault update" on public.vaults
for update using (auth.uid() = id) with check (auth.uid() = id);
drop policy if exists "own vault delete" on public.vaults;
create policy "own vault delete" on public.vaults
for delete using (auth.uid() = id);
-- -------------------------------------------------------------
-- 1b) Korumalı kasa yazımı (sunucu tarafı son yazan kazanır / last-write-wins denetleyicisi)
-- İstemci saatine körü körüne güvenen upsert yerine: Satır yalnızca
-- yeni zarfın updatedAt değeri sunucudakinden ESKİ DEĞİLSE güncellenir.
-- Saati geri kalmış bir cihaz ya da eşzamanlı yarışan iki push işlemi, daha yeni
-- veriyi sessizce ezemez. false dönerse istemci verinin eşitlenmediğini anlar.
-- -------------------------------------------------------------
create or replace function public.push_vault(envelope jsonb)
returns boolean
language plpgsql
security definer
set search_path = public
as $$
declare
uid uuid := auth.uid();
new_ts numeric := coalesce((envelope->>'updatedAt')::numeric, 0);
cur_ts numeric;
begin
if uid is null then
raise exception 'not authenticated';
end if;
select coalesce((data->>'updatedAt')::numeric, 0)
into cur_ts
from public.vaults where id = uid
for update;
if not found then
insert into public.vaults (id, data, updated_at)
values (uid, envelope, now());
return true;
end if;
if new_ts < cur_ts then
return false; -- Sunucudaki veri daha yeni; üzerine yazma
end if;
update public.vaults
set data = envelope, updated_at = now()
where id = uid;
return true;
end;
$$;
revoke all on function public.push_vault(jsonb) from public, anon;
grant execute on function public.push_vault(jsonb) to authenticated;
-- -------------------------------------------------------------
-- 2) Abonelikler (Premium — Lemon Squeezy)
-- Satırları YALNIZCA service-role (webhook) yazabilir.
-- -------------------------------------------------------------
create table if not exists public.subscriptions (
user_id uuid primary key references auth.users on delete cascade,
-- on_trial | active | past_due | cancelled | expired
status text not null default 'expired',
current_period_end timestamptz,
ls_customer_id text,
ls_subscription_id text,
-- Deneme hakkının kötüye kullanımını engeller: Bu hesap deneme süresini kullandı mı?
trial_used boolean not null default false,
updated_at timestamptz not null default now()
);
alter table public.subscriptions
add column if not exists trial_used boolean not null default false;
alter table public.subscriptions enable row level security;
drop policy if exists "own subscription select" on public.subscriptions;
create policy "own subscription select" on public.subscriptions
for select using (auth.uid() = user_id);
-- -------------------------------------------------------------
-- 3) Webhook tekrar gönderim (replay) koruması
-- Politika tanımlanmamıştır = Yalnızca service-role erişebilir.
-- -------------------------------------------------------------
create table if not exists public.webhook_events (
event_id text primary key,
received_at timestamptz not null default now()
);
alter table public.webhook_events enable row level security;
-- Not: Tablo sınırsız büyümesin diye ls-webhook fonksiyonu her çağrıda
-- 90 günden eski kayıtları temizler; bu nedenle ayrıca pg_cron gerekmez.
-- -------------------------------------------------------------
-- 4) Premium yetki denetimi (sunucu tarafı)
-- -------------------------------------------------------------
create or replace function public.is_entitled()
returns boolean
language sql
stable
security definer
set search_path = public
as $$
select exists (
select 1 from public.subscriptions s
where s.user_id = auth.uid()
and (
s.status in ('on_trial', 'active', 'past_due')
or (s.status = 'cancelled' and s.current_period_end > now())
)
-- Süresi geçmiş satır (ulaşmayan/kaçan webhook) kullanıcıyı premium saymaz
and (s.current_period_end is null or s.current_period_end > now())
);
$$;
-- -------------------------------------------------------------
-- 5) Uygulama içi hesap silme (Google Play zorunluluğu)
-- Kullanıcı yalnızca KENDİ hesabını silebilir; auth.uid() kapsamı dışına
-- çıkamaz. Ayrı bir Edge Function gerektirmez.
-- -------------------------------------------------------------
create or replace function public.delete_own_account()
returns void
language plpgsql
security definer
set search_path = public
as $$
declare
uid uuid := auth.uid();
begin
if uid is null then
raise exception 'not authenticated';
end if;
delete from public.vaults where id = uid;
delete from public.subscriptions where user_id = uid;
delete from auth.users where id = uid;
end;
$$;
revoke all on function public.delete_own_account() from public, anon;
grant execute on function public.delete_own_account() to authenticated;
-- -------------------------------------------------------------
-- 6) 6 AY ÜCRETSİZ KULLANIM (sunucu tarafında tanımlanır)
-- Her hesap, kaydolduğu andan itibaren 6 ay boyunca Premium
-- sayılır. Süre dolunca abonelik ($1/ay) gereklidir.
-- Not: Lemon Squeezy ürününde AYRICA deneme süresi tanımlamayın;
-- ücretsiz deneme dönemi doğrudan burada yönetilmektedir.
-- -------------------------------------------------------------
-- Tanıtım dönemi hesap silme döngüsüyle sıfırlanamasın: Tanıtım hakkını kullanan
-- e-posta adresinin tek yönlü kriptografik özeti (hash) saklanır (kişisel veri tutulmaz).
-- Hesap silinip aynı Google hesabıyla (yeni bir UID ile) tekrar açılsa dahi özet aynı
-- kalacağından ikinci kez 6 aylık ücretsiz hak verilmez.
create table if not exists public.redeemed_intros (
email_hash text primary key,
redeemed_at timestamptz not null default now()
);
alter table public.redeemed_intros enable row level security;
-- Politika tanımlanmamıştır = Yalnızca service-role ve security definer fonksiyonlar erişebilir.
create or replace function public.grant_intro_period()
returns trigger
language plpgsql
security definer
set search_path = public
as $$
declare
ehash text := encode(digest(lower(coalesce(new.email, new.id::text)), 'sha256'), 'hex');
begin
-- Bu e-posta adresi tanıtım hakkını daha önce kullandıysa yeni hak tanımlama
if exists (select 1 from public.redeemed_intros where email_hash = ehash) then
return new;
end if;
insert into public.redeemed_intros (email_hash) values (ehash)
on conflict (email_hash) do nothing;
insert into public.subscriptions (user_id, status, current_period_end, trial_used)
values (new.id, 'on_trial', now() + interval '6 months', true)
on conflict (user_id) do nothing;
return new;
end;
$$;
-- Mevcut hesapların hakları da kayda geçsin (yeniden kurulumda mükerrer hak tanınmasın)
insert into public.redeemed_intros (email_hash)
select encode(digest(lower(u.email), 'sha256'), 'hex')
from auth.users u
join public.subscriptions s on s.user_id = u.id
where u.email is not null and s.trial_used
on conflict (email_hash) do nothing;
drop trigger if exists on_auth_user_created_grant_intro on auth.users;
create trigger on_auth_user_created_grant_intro
after insert on auth.users
for each row execute function public.grant_intro_period();
-- Mevcut hesaplar da 6 ay ücretsiz kullanım alsın (özellik devreye girdiğinde mevcut kullanıcılar kilitlenmesin)
insert into public.subscriptions (user_id, status, current_period_end, trial_used)
select id, 'on_trial', now() + interval '6 months', true
from auth.users
on conflict (user_id) do nothing;