@marktiderman/genesis-supabase 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +377 -23
- package/dist/blocks.d.ts +19 -0
- package/dist/blocks.js +3 -0
- package/dist/blocks.js.map +1 -0
- package/dist/chunk-FAA2FTF4.js +110 -0
- package/dist/chunk-FAA2FTF4.js.map +1 -0
- package/dist/chunk-T2HGFC4C.js +37 -0
- package/dist/chunk-T2HGFC4C.js.map +1 -0
- package/dist/index-BSGz3RGk.d.ts +317 -0
- package/dist/index.d.ts +4 -103
- package/dist/index.js +60 -3
- package/dist/index.js.map +1 -1
- package/dist/native.d.ts +90 -6
- package/dist/native.js +70 -2
- package/dist/native.js.map +1 -1
- package/functions/genesis-apple-revoke/apple.ts +237 -0
- package/functions/genesis-apple-revoke/handler.ts +227 -0
- package/functions/genesis-apple-revoke/index.ts +55 -0
- package/functions/genesis-push-send/expo.ts +117 -0
- package/functions/genesis-push-send/handler.ts +462 -0
- package/functions/genesis-push-send/index.ts +54 -0
- package/functions/genesis-support-cleanup/handler.ts +80 -0
- package/functions/genesis-support-cleanup/index.ts +39 -0
- package/functions/genesis-support-intake/contract.ts +193 -0
- package/functions/genesis-support-intake/handler.ts +170 -0
- package/functions/genesis-support-intake/index.ts +47 -0
- package/functions/genesis-support-notify/handler.ts +167 -0
- package/functions/genesis-support-notify/index.ts +15 -0
- package/functions/tests/genesis-apple-revoke_test.ts +81 -0
- package/functions/tests/genesis-push-send_test.ts +53 -0
- package/migrations/0003_genesis_apple_token_revocation.sql +205 -0
- package/migrations/0004_genesis_tenant_features.sql +333 -0
- package/migrations/0005_genesis_push.sql +524 -0
- package/migrations/0006_genesis_support.sql +846 -0
- package/migrations/tests/0003_genesis_apple_token_revocation.test.sql +230 -0
- package/migrations/tests/0004_genesis_tenant_features.test.sql +418 -0
- package/migrations/tests/0005_genesis_push.test.sql +407 -0
- package/migrations/tests/0006_genesis_support.test.sql +552 -0
- package/package.json +91 -4
|
@@ -0,0 +1,846 @@
|
|
|
1
|
+
-- Genesis support: feedback reports, screenshots, staff replies, an idea board and votes
|
|
2
|
+
--
|
|
3
|
+
-- The server half of the support block (`@marktiderman/genesis-support`), in the app's
|
|
4
|
+
-- own Supabase:
|
|
5
|
+
--
|
|
6
|
+
-- public.support_reports what a person sends (a bug, an idea, a question or praise)
|
|
7
|
+
-- with the context the app collected (version signature,
|
|
8
|
+
-- route, device), a status and an optional FeatureId
|
|
9
|
+
-- public.support_attachments a report's screenshots, kept in the private
|
|
10
|
+
-- `genesis-support` storage bucket under <user id>/<report id>/
|
|
11
|
+
-- public.support_replies the conversation on a report, between the person and staff
|
|
12
|
+
-- public.support_ideas the idea board: anyone signed in suggests and reads, staff
|
|
13
|
+
-- set the status (shipped links the FeatureId that delivered it)
|
|
14
|
+
-- public.support_idea_votes one vote per person per idea
|
|
15
|
+
-- public.support_attachment_purges
|
|
16
|
+
-- storage objects to remove after their attachment row went
|
|
17
|
+
-- (a deleted report or account); the genesis-support-cleanup
|
|
18
|
+
-- function removes them through the Storage API
|
|
19
|
+
--
|
|
20
|
+
-- Access, enforced here by row level security and column privileges:
|
|
21
|
+
-- - A person sees, edits (while it is open) and deletes their own reports, replies on
|
|
22
|
+
-- them and sees their own screenshots. Nobody else does, except staff.
|
|
23
|
+
-- - Every signed-in person reads the idea board. A person edits their own idea while it
|
|
24
|
+
-- is open. Votes: one per person and idea, and a person sees only their own; the count
|
|
25
|
+
-- is on the idea, kept by a trigger. Supabase anonymous sign-ins (`is_anonymous`) may
|
|
26
|
+
-- report but never vote or suggest, so minting throwaway logins can't stuff the board.
|
|
27
|
+
-- - Staff read every report and screenshot, reply, set statuses and hide ideas. Beyond
|
|
28
|
+
-- reading and replying, staff act only through the functions below.
|
|
29
|
+
-- - anon (no session) reaches nothing.
|
|
30
|
+
--
|
|
31
|
+
-- The app hook, like the tenant-admin hook in 0004: staff are whoever
|
|
32
|
+
-- `private.genesis_support_staff(person uuid) returns boolean` says. Without it nobody is
|
|
33
|
+
-- staff, so a fresh install fails closed. It runs with this file's (the owner's) rights,
|
|
34
|
+
-- so it reads your membership table whatever its RLS:
|
|
35
|
+
--
|
|
36
|
+
-- create function private.genesis_support_staff(person uuid) returns boolean
|
|
37
|
+
-- language sql stable set search_path = '' as $$
|
|
38
|
+
-- select exists (select 1 from public.staff s where s.user_id = person)
|
|
39
|
+
-- $$;
|
|
40
|
+
-- revoke execute on function private.genesis_support_staff(uuid) from public, anon, authenticated;
|
|
41
|
+
--
|
|
42
|
+
-- Limits (each raises SQLSTATE GS429): 20 reports and 60 replies per person per hour,
|
|
43
|
+
-- 10 ideas per person per day, 5 screenshots per report. They count what was sent, in a log
|
|
44
|
+
-- deleting doesn't touch, under a per-person lock, so deleting and sending again doesn't reset
|
|
45
|
+
-- them. Text and jsonb are capped by checks.
|
|
46
|
+
--
|
|
47
|
+
-- The bucket stays private whatever other storage policies the app has: restrictive policies
|
|
48
|
+
-- hold every role but the service role to this file's rules inside `genesis-support`.
|
|
49
|
+
--
|
|
50
|
+
-- Account deletion (0001): every row here goes with the login (reports, the replies on
|
|
51
|
+
-- them, screenshots and votes cascade; ideas and replies on other people's reports keep
|
|
52
|
+
-- their text without the person). A deleted screenshot's storage object is queued in
|
|
53
|
+
-- support_attachment_purges, because Supabase refuses direct deletes from storage.objects.
|
|
54
|
+
-- Supabase documents that a login owning Storage objects can't be deleted (see 0001): where
|
|
55
|
+
-- that holds, delete the person's reports first (their screenshots are queued) and run the
|
|
56
|
+
-- cleanup function, then delete the account. Not verified against a hosted project.
|
|
57
|
+
--
|
|
58
|
+
-- Use: copy this file into your app's `supabase/migrations/` under a new timestamped
|
|
59
|
+
-- name. Never edit it once applied; change it with a new migration. It stands alone (it
|
|
60
|
+
-- doesn't need 0001 to 0005).
|
|
61
|
+
--
|
|
62
|
+
-- Verify: the pgTAP test beside this file (`tests/0006_genesis_support.test.sql`).
|
|
63
|
+
|
|
64
|
+
create schema if not exists private;
|
|
65
|
+
|
|
66
|
+
-- Helpers -----------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
-- Is the signed-in person staff? Missing hook, nobody signed in, or a null answer: false.
|
|
69
|
+
-- In public so policies can call it; it answers only about the caller.
|
|
70
|
+
create or replace function public.support_is_staff()
|
|
71
|
+
returns boolean
|
|
72
|
+
language plpgsql
|
|
73
|
+
stable
|
|
74
|
+
security definer
|
|
75
|
+
set search_path = ''
|
|
76
|
+
as $$
|
|
77
|
+
declare
|
|
78
|
+
me uuid := auth.uid();
|
|
79
|
+
begin
|
|
80
|
+
if me is null or to_regprocedure('private.genesis_support_staff(uuid)') is null then
|
|
81
|
+
return false;
|
|
82
|
+
end if;
|
|
83
|
+
return coalesce(private.genesis_support_staff(me), false);
|
|
84
|
+
end;
|
|
85
|
+
$$;
|
|
86
|
+
|
|
87
|
+
comment on function public.support_is_staff() is
|
|
88
|
+
'True when the signed-in person is support staff (private.genesis_support_staff). Genesis support.';
|
|
89
|
+
|
|
90
|
+
-- Is the signed-in person a Supabase anonymous sign-in? Read from auth.users, not from the
|
|
91
|
+
-- JWT, so a custom access-token hook that drops the claim can't hide it. Answers only about the caller.
|
|
92
|
+
create or replace function public.support_is_anonymous()
|
|
93
|
+
returns boolean
|
|
94
|
+
language sql
|
|
95
|
+
stable
|
|
96
|
+
security definer
|
|
97
|
+
set search_path = ''
|
|
98
|
+
as $$
|
|
99
|
+
select coalesce((select u.is_anonymous from auth.users u where u.id = auth.uid()), true)
|
|
100
|
+
$$;
|
|
101
|
+
|
|
102
|
+
comment on function public.support_is_anonymous() is
|
|
103
|
+
'True when the signed-in person is a Supabase anonymous sign-in (or nobody). Genesis support.';
|
|
104
|
+
|
|
105
|
+
-- What each person sent, for the limits: kept when the report is deleted, gone with the login.
|
|
106
|
+
create table private.support_rate_events (
|
|
107
|
+
user_id uuid not null references auth.users (id) on delete cascade,
|
|
108
|
+
kind text not null,
|
|
109
|
+
at timestamptz not null default clock_timestamp()
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
create index support_rate_events_lookup on private.support_rate_events (user_id, kind, at desc);
|
|
113
|
+
|
|
114
|
+
-- Counts the person's sends of `kind` within `window_size`, refuses past `max_count`, then
|
|
115
|
+
-- records this one. A per-person lock makes parallel sends wait their turn.
|
|
116
|
+
create or replace function private.genesis_support_rate_limit(person uuid, kind text, max_count integer, window_size interval)
|
|
117
|
+
returns void
|
|
118
|
+
language plpgsql
|
|
119
|
+
security definer
|
|
120
|
+
set search_path = ''
|
|
121
|
+
as $$
|
|
122
|
+
begin
|
|
123
|
+
if person is null then
|
|
124
|
+
return;
|
|
125
|
+
end if;
|
|
126
|
+
perform pg_advisory_xact_lock(hashtextextended('genesis_support:' || person::text, 0));
|
|
127
|
+
delete from private.support_rate_events e where e.user_id = person and e.at < clock_timestamp() - interval '2 days';
|
|
128
|
+
if (select count(*) from private.support_rate_events e
|
|
129
|
+
where e.user_id = person and e.kind = genesis_support_rate_limit.kind and e.at > clock_timestamp() - window_size) >= max_count then
|
|
130
|
+
raise exception 'support: too many % sent; try again later', kind using errcode = 'GS429';
|
|
131
|
+
end if;
|
|
132
|
+
insert into private.support_rate_events (user_id, kind) values (person, kind);
|
|
133
|
+
end;
|
|
134
|
+
$$;
|
|
135
|
+
|
|
136
|
+
-- updated_at moves when what was said or decided changes, not when a thread is read or a
|
|
137
|
+
-- vote is counted.
|
|
138
|
+
create or replace function private.genesis_support_touch()
|
|
139
|
+
returns trigger
|
|
140
|
+
language plpgsql
|
|
141
|
+
set search_path = ''
|
|
142
|
+
as $$
|
|
143
|
+
begin
|
|
144
|
+
if (to_jsonb(new) - array['updated_at', 'user_read_at', 'staff_read_at', 'last_user_reply_at', 'last_staff_reply_at', 'vote_count'])
|
|
145
|
+
is distinct from (to_jsonb(old) - array['updated_at', 'user_read_at', 'staff_read_at', 'last_user_reply_at', 'last_staff_reply_at', 'vote_count']) then
|
|
146
|
+
new.updated_at := now();
|
|
147
|
+
end if;
|
|
148
|
+
return new;
|
|
149
|
+
end;
|
|
150
|
+
$$;
|
|
151
|
+
|
|
152
|
+
-- Reports -----------------------------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
create table public.support_reports (
|
|
155
|
+
id uuid primary key default gen_random_uuid(),
|
|
156
|
+
user_id uuid not null default auth.uid() references auth.users (id) on delete cascade,
|
|
157
|
+
type text not null default 'bug'
|
|
158
|
+
constraint support_reports_type check (type in ('bug', 'idea', 'question', 'praise')),
|
|
159
|
+
title text
|
|
160
|
+
constraint support_reports_title_length check (char_length(title) <= 300),
|
|
161
|
+
message text not null
|
|
162
|
+
constraint support_reports_message_length check (char_length(btrim(message)) between 1 and 20000),
|
|
163
|
+
-- The screen, as a path: never a query string or fragment (tokens, codes and emails live there).
|
|
164
|
+
route text
|
|
165
|
+
constraint support_reports_route_shape check (char_length(route) <= 2000 and route !~ '[?#]'),
|
|
166
|
+
-- The version signature (`1.4.0 (37) · preview · 3f2a9c1b`).
|
|
167
|
+
app_version text
|
|
168
|
+
constraint support_reports_app_version_length check (char_length(app_version) <= 200),
|
|
169
|
+
platform text
|
|
170
|
+
constraint support_reports_platform_length check (char_length(platform) <= 40),
|
|
171
|
+
device jsonb not null default '{}'::jsonb
|
|
172
|
+
constraint support_reports_device_shape check (jsonb_typeof(device) = 'object' and pg_column_size(device) <= 4096),
|
|
173
|
+
context jsonb not null default '{}'::jsonb
|
|
174
|
+
constraint support_reports_context_shape check (jsonb_typeof(context) = 'object' and pg_column_size(context) <= 16384),
|
|
175
|
+
status text not null default 'open'
|
|
176
|
+
constraint support_reports_status check (status in ('open', 'submitted', 'in_progress', 'addressed', 'wont_fix')),
|
|
177
|
+
-- The FeatureId (features.ts) the report is about, set by staff.
|
|
178
|
+
feature_id text
|
|
179
|
+
constraint support_reports_feature_id_format check (feature_id ~ '^[a-z][a-z0-9]*([-_.][a-z0-9]+)*$' and char_length(feature_id) <= 100),
|
|
180
|
+
last_user_reply_at timestamptz,
|
|
181
|
+
last_staff_reply_at timestamptz,
|
|
182
|
+
-- Read receipts: when the person, and when any staff member, last read the thread.
|
|
183
|
+
user_read_at timestamptz,
|
|
184
|
+
staff_read_at timestamptz,
|
|
185
|
+
created_at timestamptz not null default now(),
|
|
186
|
+
updated_at timestamptz not null default now()
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
create index support_reports_user on public.support_reports (user_id, created_at desc);
|
|
190
|
+
create index support_reports_status on public.support_reports (status, created_at desc);
|
|
191
|
+
|
|
192
|
+
comment on table public.support_reports is
|
|
193
|
+
'Feedback a person sent from the app, with its context and status. Their own; staff read all. Genesis support.';
|
|
194
|
+
|
|
195
|
+
create trigger genesis_support_touch
|
|
196
|
+
before update on public.support_reports
|
|
197
|
+
for each row execute function private.genesis_support_touch();
|
|
198
|
+
|
|
199
|
+
-- A new report starts open, about no feature, with no replies, whatever was sent; a
|
|
200
|
+
-- person sends at most 20 an hour.
|
|
201
|
+
create or replace function private.genesis_support_new_report()
|
|
202
|
+
returns trigger
|
|
203
|
+
language plpgsql
|
|
204
|
+
security definer
|
|
205
|
+
set search_path = ''
|
|
206
|
+
as $$
|
|
207
|
+
begin
|
|
208
|
+
new.status := 'open';
|
|
209
|
+
new.feature_id := null;
|
|
210
|
+
new.last_user_reply_at := null;
|
|
211
|
+
new.last_staff_reply_at := null;
|
|
212
|
+
new.user_read_at := null;
|
|
213
|
+
new.staff_read_at := null;
|
|
214
|
+
new.created_at := now();
|
|
215
|
+
new.updated_at := now();
|
|
216
|
+
perform private.genesis_support_rate_limit(new.user_id, 'report', 20, interval '1 hour');
|
|
217
|
+
return new;
|
|
218
|
+
end;
|
|
219
|
+
$$;
|
|
220
|
+
|
|
221
|
+
create trigger genesis_support_new_report
|
|
222
|
+
before insert on public.support_reports
|
|
223
|
+
for each row execute function private.genesis_support_new_report();
|
|
224
|
+
|
|
225
|
+
-- Attachments -------------------------------------------------------------------------
|
|
226
|
+
|
|
227
|
+
create table public.support_attachments (
|
|
228
|
+
id uuid primary key default gen_random_uuid(),
|
|
229
|
+
report_id uuid not null references public.support_reports (id) on delete cascade,
|
|
230
|
+
user_id uuid not null default auth.uid() references auth.users (id) on delete cascade,
|
|
231
|
+
bucket text not null default 'genesis-support'
|
|
232
|
+
constraint support_attachments_bucket check (bucket = 'genesis-support'),
|
|
233
|
+
-- <user id>/<report id>/<name>.<png|jpg|jpeg|webp>
|
|
234
|
+
path text not null unique
|
|
235
|
+
constraint support_attachments_path_shape check (path ~ '^[0-9a-f-]{36}/[0-9a-f-]{36}/[A-Za-z0-9_-]{1,80}\.(png|jpg|jpeg|webp)$'),
|
|
236
|
+
content_type text not null
|
|
237
|
+
constraint support_attachments_content_type check (content_type in ('image/png', 'image/jpeg', 'image/webp')),
|
|
238
|
+
byte_size integer
|
|
239
|
+
constraint support_attachments_byte_size check (byte_size between 1 and 5242880),
|
|
240
|
+
width integer
|
|
241
|
+
constraint support_attachments_width check (width between 1 and 10000),
|
|
242
|
+
height integer
|
|
243
|
+
constraint support_attachments_height check (height between 1 and 10000),
|
|
244
|
+
created_at timestamptz not null default now()
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
create index support_attachments_report on public.support_attachments (report_id);
|
|
248
|
+
create index support_attachments_user on public.support_attachments (user_id);
|
|
249
|
+
|
|
250
|
+
comment on table public.support_attachments is
|
|
251
|
+
'Screenshots of a report, in the private genesis-support bucket. Genesis support.';
|
|
252
|
+
|
|
253
|
+
-- An attachment names an object the person already uploaded under their own report's folder.
|
|
254
|
+
create or replace function private.genesis_support_new_attachment()
|
|
255
|
+
returns trigger
|
|
256
|
+
language plpgsql
|
|
257
|
+
security definer
|
|
258
|
+
set search_path = ''
|
|
259
|
+
as $$
|
|
260
|
+
begin
|
|
261
|
+
new.bucket := 'genesis-support';
|
|
262
|
+
new.created_at := now();
|
|
263
|
+
if new.path is null or split_part(new.path, '/', 1) <> new.user_id::text or split_part(new.path, '/', 2) <> new.report_id::text then
|
|
264
|
+
raise exception 'support: an attachment lives under <user id>/<report id>/' using errcode = '42501';
|
|
265
|
+
end if;
|
|
266
|
+
if not exists (select 1 from public.support_reports r where r.id = new.report_id and r.user_id = new.user_id) then
|
|
267
|
+
raise exception 'support: not your report' using errcode = '42501';
|
|
268
|
+
end if;
|
|
269
|
+
if not exists (select 1 from storage.objects o where o.bucket_id = 'genesis-support' and o.name = new.path) then
|
|
270
|
+
raise exception 'support: upload the file before recording it' using errcode = '23503';
|
|
271
|
+
end if;
|
|
272
|
+
if (select count(*) from public.support_attachments a where a.report_id = new.report_id) >= 5 then
|
|
273
|
+
raise exception 'support: at most 5 screenshots per report' using errcode = 'GS429';
|
|
274
|
+
end if;
|
|
275
|
+
return new;
|
|
276
|
+
end;
|
|
277
|
+
$$;
|
|
278
|
+
|
|
279
|
+
create trigger genesis_support_new_attachment
|
|
280
|
+
before insert on public.support_attachments
|
|
281
|
+
for each row execute function private.genesis_support_new_attachment();
|
|
282
|
+
|
|
283
|
+
-- Objects whose attachment row is gone, for the cleanup function (service role) to remove
|
|
284
|
+
-- through the Storage API. Nobody else reads it.
|
|
285
|
+
create table public.support_attachment_purges (
|
|
286
|
+
id bigint generated always as identity primary key,
|
|
287
|
+
bucket text not null,
|
|
288
|
+
path text not null,
|
|
289
|
+
queued_at timestamptz not null default now()
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
comment on table public.support_attachment_purges is
|
|
293
|
+
'Storage objects to remove after their support attachment was deleted. Service role only. Genesis support.';
|
|
294
|
+
|
|
295
|
+
create or replace function private.genesis_support_queue_purge()
|
|
296
|
+
returns trigger
|
|
297
|
+
language plpgsql
|
|
298
|
+
security definer
|
|
299
|
+
set search_path = ''
|
|
300
|
+
as $$
|
|
301
|
+
begin
|
|
302
|
+
insert into public.support_attachment_purges (bucket, path) values (old.bucket, old.path);
|
|
303
|
+
return null;
|
|
304
|
+
end;
|
|
305
|
+
$$;
|
|
306
|
+
|
|
307
|
+
create trigger genesis_support_queue_purge
|
|
308
|
+
after delete on public.support_attachments
|
|
309
|
+
for each row execute function private.genesis_support_queue_purge();
|
|
310
|
+
|
|
311
|
+
-- Replies -----------------------------------------------------------------------------
|
|
312
|
+
|
|
313
|
+
create table public.support_replies (
|
|
314
|
+
id uuid primary key default gen_random_uuid(),
|
|
315
|
+
report_id uuid not null references public.support_reports (id) on delete cascade,
|
|
316
|
+
author_id uuid default auth.uid() references auth.users (id) on delete set null,
|
|
317
|
+
from_staff boolean not null default false,
|
|
318
|
+
body text not null
|
|
319
|
+
constraint support_replies_body_length check (char_length(btrim(body)) between 1 and 10000),
|
|
320
|
+
created_at timestamptz not null default now()
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
create index support_replies_report on public.support_replies (report_id, created_at);
|
|
324
|
+
create index support_replies_author on public.support_replies (author_id, created_at desc);
|
|
325
|
+
|
|
326
|
+
comment on table public.support_replies is
|
|
327
|
+
'The conversation on a report, between the person and staff. Genesis support.';
|
|
328
|
+
|
|
329
|
+
-- Who wrote it, and whether as staff, come from the session, never from the request.
|
|
330
|
+
create or replace function private.genesis_support_new_reply()
|
|
331
|
+
returns trigger
|
|
332
|
+
language plpgsql
|
|
333
|
+
security definer
|
|
334
|
+
set search_path = ''
|
|
335
|
+
as $$
|
|
336
|
+
declare
|
|
337
|
+
me uuid := auth.uid();
|
|
338
|
+
owner uuid;
|
|
339
|
+
staff boolean := public.support_is_staff();
|
|
340
|
+
begin
|
|
341
|
+
select r.user_id into owner from public.support_reports r where r.id = new.report_id;
|
|
342
|
+
if me is null or owner is null or (owner <> me and not staff) then
|
|
343
|
+
raise exception 'support: not your report' using errcode = '42501';
|
|
344
|
+
end if;
|
|
345
|
+
new.author_id := me;
|
|
346
|
+
-- On their own report a staff member writes as the person.
|
|
347
|
+
new.from_staff := staff and owner <> me;
|
|
348
|
+
new.created_at := clock_timestamp();
|
|
349
|
+
perform private.genesis_support_rate_limit(me, 'reply', 60, interval '1 hour');
|
|
350
|
+
return new;
|
|
351
|
+
end;
|
|
352
|
+
$$;
|
|
353
|
+
|
|
354
|
+
create trigger genesis_support_new_reply
|
|
355
|
+
before insert on public.support_replies
|
|
356
|
+
for each row execute function private.genesis_support_new_reply();
|
|
357
|
+
|
|
358
|
+
-- A reply moves the thread on: the writer's side has read it, the other side has something new.
|
|
359
|
+
create or replace function private.genesis_support_reply_posted()
|
|
360
|
+
returns trigger
|
|
361
|
+
language plpgsql
|
|
362
|
+
security definer
|
|
363
|
+
set search_path = ''
|
|
364
|
+
as $$
|
|
365
|
+
begin
|
|
366
|
+
if new.from_staff then
|
|
367
|
+
update public.support_reports
|
|
368
|
+
set last_staff_reply_at = new.created_at, staff_read_at = new.created_at
|
|
369
|
+
where id = new.report_id;
|
|
370
|
+
else
|
|
371
|
+
update public.support_reports
|
|
372
|
+
set last_user_reply_at = new.created_at, user_read_at = new.created_at
|
|
373
|
+
where id = new.report_id;
|
|
374
|
+
end if;
|
|
375
|
+
return null;
|
|
376
|
+
end;
|
|
377
|
+
$$;
|
|
378
|
+
|
|
379
|
+
create trigger genesis_support_reply_posted
|
|
380
|
+
after insert on public.support_replies
|
|
381
|
+
for each row execute function private.genesis_support_reply_posted();
|
|
382
|
+
|
|
383
|
+
-- Ideas and votes ---------------------------------------------------------------------
|
|
384
|
+
|
|
385
|
+
create table public.support_ideas (
|
|
386
|
+
id uuid primary key default gen_random_uuid(),
|
|
387
|
+
author_id uuid default auth.uid() references auth.users (id) on delete set null,
|
|
388
|
+
title text not null
|
|
389
|
+
constraint support_ideas_title_length check (char_length(btrim(title)) between 3 and 140),
|
|
390
|
+
body text
|
|
391
|
+
constraint support_ideas_body_length check (char_length(body) <= 4000),
|
|
392
|
+
-- hidden: taken off the board by staff (spam, a duplicate); its author and staff still see it.
|
|
393
|
+
status text not null default 'open'
|
|
394
|
+
constraint support_ideas_status check (status in ('open', 'planned', 'in_progress', 'shipped', 'declined', 'hidden')),
|
|
395
|
+
-- The FeatureId (features.ts) that delivers it, set by staff.
|
|
396
|
+
feature_id text
|
|
397
|
+
constraint support_ideas_feature_id_format check (feature_id ~ '^[a-z][a-z0-9]*([-_.][a-z0-9]+)*$' and char_length(feature_id) <= 100),
|
|
398
|
+
shipped_at timestamptz,
|
|
399
|
+
vote_count integer not null default 0,
|
|
400
|
+
created_at timestamptz not null default now(),
|
|
401
|
+
updated_at timestamptz not null default now()
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
create index support_ideas_board on public.support_ideas (status, vote_count desc, created_at desc);
|
|
405
|
+
create index support_ideas_author on public.support_ideas (author_id, created_at desc);
|
|
406
|
+
|
|
407
|
+
comment on table public.support_ideas is
|
|
408
|
+
'The idea board: ideas every signed-in person reads and votes on, with a status staff set. Genesis support.';
|
|
409
|
+
|
|
410
|
+
create trigger genesis_support_touch
|
|
411
|
+
before update on public.support_ideas
|
|
412
|
+
for each row execute function private.genesis_support_touch();
|
|
413
|
+
|
|
414
|
+
create or replace function private.genesis_support_new_idea()
|
|
415
|
+
returns trigger
|
|
416
|
+
language plpgsql
|
|
417
|
+
security definer
|
|
418
|
+
set search_path = ''
|
|
419
|
+
as $$
|
|
420
|
+
begin
|
|
421
|
+
new.status := 'open';
|
|
422
|
+
new.feature_id := null;
|
|
423
|
+
new.shipped_at := null;
|
|
424
|
+
new.vote_count := 0;
|
|
425
|
+
new.created_at := now();
|
|
426
|
+
new.updated_at := now();
|
|
427
|
+
perform private.genesis_support_rate_limit(new.author_id, 'idea', 10, interval '1 day');
|
|
428
|
+
return new;
|
|
429
|
+
end;
|
|
430
|
+
$$;
|
|
431
|
+
|
|
432
|
+
create trigger genesis_support_new_idea
|
|
433
|
+
before insert on public.support_ideas
|
|
434
|
+
for each row execute function private.genesis_support_new_idea();
|
|
435
|
+
|
|
436
|
+
create table public.support_idea_votes (
|
|
437
|
+
idea_id uuid not null references public.support_ideas (id) on delete cascade,
|
|
438
|
+
user_id uuid not null default auth.uid() references auth.users (id) on delete cascade,
|
|
439
|
+
created_at timestamptz not null default now(),
|
|
440
|
+
primary key (idea_id, user_id)
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
create index support_idea_votes_user on public.support_idea_votes (user_id, created_at desc);
|
|
444
|
+
|
|
445
|
+
comment on table public.support_idea_votes is
|
|
446
|
+
'One vote per person per idea. A person sees only their own; the count is on the idea. Genesis support.';
|
|
447
|
+
|
|
448
|
+
create or replace function private.genesis_support_count_vote()
|
|
449
|
+
returns trigger
|
|
450
|
+
language plpgsql
|
|
451
|
+
security definer
|
|
452
|
+
set search_path = ''
|
|
453
|
+
as $$
|
|
454
|
+
begin
|
|
455
|
+
if tg_op = 'INSERT' then
|
|
456
|
+
update public.support_ideas set vote_count = vote_count + 1 where id = new.idea_id;
|
|
457
|
+
else
|
|
458
|
+
update public.support_ideas set vote_count = greatest(vote_count - 1, 0) where id = old.idea_id;
|
|
459
|
+
end if;
|
|
460
|
+
return null;
|
|
461
|
+
end;
|
|
462
|
+
$$;
|
|
463
|
+
|
|
464
|
+
create trigger genesis_support_count_vote
|
|
465
|
+
after insert or delete on public.support_idea_votes
|
|
466
|
+
for each row execute function private.genesis_support_count_vote();
|
|
467
|
+
|
|
468
|
+
-- Row level security ------------------------------------------------------------------
|
|
469
|
+
|
|
470
|
+
alter table public.support_reports enable row level security;
|
|
471
|
+
alter table public.support_attachments enable row level security;
|
|
472
|
+
alter table public.support_attachment_purges enable row level security;
|
|
473
|
+
alter table public.support_replies enable row level security;
|
|
474
|
+
alter table public.support_ideas enable row level security;
|
|
475
|
+
alter table public.support_idea_votes enable row level security;
|
|
476
|
+
|
|
477
|
+
create policy support_reports_read on public.support_reports
|
|
478
|
+
for select to authenticated
|
|
479
|
+
using (user_id = (select auth.uid()) or (select public.support_is_staff()));
|
|
480
|
+
create policy support_reports_send on public.support_reports
|
|
481
|
+
for insert to authenticated
|
|
482
|
+
with check (user_id = (select auth.uid()));
|
|
483
|
+
-- Only until staff pick it up or answer: nobody's reply ends up under words it didn't answer.
|
|
484
|
+
create policy support_reports_edit on public.support_reports
|
|
485
|
+
for update to authenticated
|
|
486
|
+
using (user_id = (select auth.uid()) and status = 'open' and last_staff_reply_at is null)
|
|
487
|
+
with check (user_id = (select auth.uid()) and status = 'open' and last_staff_reply_at is null);
|
|
488
|
+
create policy support_reports_delete on public.support_reports
|
|
489
|
+
for delete to authenticated
|
|
490
|
+
using (user_id = (select auth.uid()));
|
|
491
|
+
|
|
492
|
+
create policy support_attachments_read on public.support_attachments
|
|
493
|
+
for select to authenticated
|
|
494
|
+
using (user_id = (select auth.uid()) or (select public.support_is_staff()));
|
|
495
|
+
create policy support_attachments_add on public.support_attachments
|
|
496
|
+
for insert to authenticated
|
|
497
|
+
with check (user_id = (select auth.uid()));
|
|
498
|
+
|
|
499
|
+
create policy support_replies_read on public.support_replies
|
|
500
|
+
for select to authenticated
|
|
501
|
+
using (
|
|
502
|
+
(select public.support_is_staff())
|
|
503
|
+
or exists (select 1 from public.support_reports r where r.id = report_id and r.user_id = (select auth.uid()))
|
|
504
|
+
);
|
|
505
|
+
create policy support_replies_send on public.support_replies
|
|
506
|
+
for insert to authenticated
|
|
507
|
+
with check (
|
|
508
|
+
(select public.support_is_staff())
|
|
509
|
+
or exists (select 1 from public.support_reports r where r.id = report_id and r.user_id = (select auth.uid()))
|
|
510
|
+
);
|
|
511
|
+
|
|
512
|
+
create policy support_ideas_read on public.support_ideas
|
|
513
|
+
for select to authenticated
|
|
514
|
+
using (status <> 'hidden' or author_id = (select auth.uid()) or (select public.support_is_staff()));
|
|
515
|
+
create policy support_ideas_suggest on public.support_ideas
|
|
516
|
+
for insert to authenticated
|
|
517
|
+
with check (
|
|
518
|
+
author_id = (select auth.uid())
|
|
519
|
+
and not (select public.support_is_anonymous())
|
|
520
|
+
);
|
|
521
|
+
-- Only until the first vote: nobody's vote moves to an idea they didn't vote for.
|
|
522
|
+
create policy support_ideas_edit on public.support_ideas
|
|
523
|
+
for update to authenticated
|
|
524
|
+
using (author_id = (select auth.uid()) and status = 'open' and vote_count = 0)
|
|
525
|
+
with check (author_id = (select auth.uid()) and status = 'open' and vote_count = 0);
|
|
526
|
+
|
|
527
|
+
create policy support_idea_votes_read on public.support_idea_votes
|
|
528
|
+
for select to authenticated
|
|
529
|
+
using (user_id = (select auth.uid()));
|
|
530
|
+
create policy support_idea_votes_cast on public.support_idea_votes
|
|
531
|
+
for insert to authenticated
|
|
532
|
+
with check (
|
|
533
|
+
user_id = (select auth.uid())
|
|
534
|
+
and not (select public.support_is_anonymous())
|
|
535
|
+
and exists (select 1 from public.support_ideas i where i.id = idea_id and i.status in ('open', 'planned', 'in_progress'))
|
|
536
|
+
);
|
|
537
|
+
create policy support_idea_votes_withdraw on public.support_idea_votes
|
|
538
|
+
for delete to authenticated
|
|
539
|
+
using (user_id = (select auth.uid()));
|
|
540
|
+
|
|
541
|
+
-- The storage bucket: private, images only, 5 MB each ---------------------------------
|
|
542
|
+
|
|
543
|
+
insert into storage.buckets (id, name, public, file_size_limit, allowed_mime_types)
|
|
544
|
+
values ('genesis-support', 'genesis-support', false, 5242880, array['image/png', 'image/jpeg', 'image/webp'])
|
|
545
|
+
on conflict (id) do update
|
|
546
|
+
set public = false, file_size_limit = excluded.file_size_limit, allowed_mime_types = excluded.allowed_mime_types;
|
|
547
|
+
|
|
548
|
+
-- May the signed-in person upload `object_name`? Only `<own id>/<own open report>/<name>.<image>`,
|
|
549
|
+
-- within an hour of sending the report, and at most 5 objects in its folder. A function, so the
|
|
550
|
+
-- policy can count the folder.
|
|
551
|
+
create or replace function public.support_can_upload(object_name text)
|
|
552
|
+
returns boolean
|
|
553
|
+
language plpgsql
|
|
554
|
+
stable
|
|
555
|
+
security definer
|
|
556
|
+
set search_path = ''
|
|
557
|
+
as $$
|
|
558
|
+
declare
|
|
559
|
+
me uuid := auth.uid();
|
|
560
|
+
folder text;
|
|
561
|
+
begin
|
|
562
|
+
if me is null or object_name is null
|
|
563
|
+
or object_name !~ '^[0-9a-f-]{36}/[0-9a-f-]{36}/[A-Za-z0-9_-]{1,80}\.(png|jpg|jpeg|webp)$'
|
|
564
|
+
or split_part(object_name, '/', 1) <> me::text then
|
|
565
|
+
return false;
|
|
566
|
+
end if;
|
|
567
|
+
if not exists (select 1 from public.support_reports r
|
|
568
|
+
where r.id::text = split_part(object_name, '/', 2) and r.user_id = me and r.status = 'open'
|
|
569
|
+
and r.created_at > now() - interval '1 hour') then
|
|
570
|
+
return false;
|
|
571
|
+
end if;
|
|
572
|
+
folder := split_part(object_name, '/', 1) || '/' || split_part(object_name, '/', 2) || '/';
|
|
573
|
+
return (select count(*) from storage.objects o where o.bucket_id = 'genesis-support' and left(o.name, length(folder)) = folder) < 5;
|
|
574
|
+
end;
|
|
575
|
+
$$;
|
|
576
|
+
|
|
577
|
+
comment on function public.support_can_upload(text) is
|
|
578
|
+
'Whether the signed-in person may upload this support screenshot path. Genesis support.';
|
|
579
|
+
|
|
580
|
+
-- May the signed-in person read `object_name`? Their own while it is attached (or within an
|
|
581
|
+
-- hour of uploading it, before the attachment row lands); staff, only while it is attached.
|
|
582
|
+
-- A deleted report's screenshots can't be opened again, before the cleanup removes them.
|
|
583
|
+
create or replace function public.support_can_read(object_name text, uploaded_at timestamptz)
|
|
584
|
+
returns boolean
|
|
585
|
+
language sql
|
|
586
|
+
stable
|
|
587
|
+
security definer
|
|
588
|
+
set search_path = ''
|
|
589
|
+
as $$
|
|
590
|
+
select auth.uid() is not null and (
|
|
591
|
+
exists (select 1 from public.support_attachments a where a.path = object_name)
|
|
592
|
+
and (split_part(object_name, '/', 1) = auth.uid()::text or public.support_is_staff())
|
|
593
|
+
or (split_part(object_name, '/', 1) = auth.uid()::text and uploaded_at > now() - interval '1 hour')
|
|
594
|
+
)
|
|
595
|
+
$$;
|
|
596
|
+
|
|
597
|
+
comment on function public.support_can_read(text, timestamptz) is
|
|
598
|
+
'Whether the signed-in person may read this support screenshot. Genesis support.';
|
|
599
|
+
|
|
600
|
+
-- Upload only into <your id>/<your open report>/; read your own, or any attached one as staff.
|
|
601
|
+
-- No update policy (nothing is overwritten or moved) and no delete policy (removal is the
|
|
602
|
+
-- cleanup function's, through the Storage API).
|
|
603
|
+
create policy genesis_support_upload on storage.objects
|
|
604
|
+
for insert to authenticated
|
|
605
|
+
with check (bucket_id = 'genesis-support' and (select public.support_can_upload(name)));
|
|
606
|
+
create policy genesis_support_read on storage.objects
|
|
607
|
+
for select to authenticated
|
|
608
|
+
using (bucket_id = 'genesis-support' and public.support_can_read(name, created_at));
|
|
609
|
+
|
|
610
|
+
-- Restrictive: whatever permissive storage policies the app already has (a "read everything",
|
|
611
|
+
-- an "owners may update"), inside this bucket only the rules above hold, for every role but
|
|
612
|
+
-- the service role (which bypasses row level security).
|
|
613
|
+
create policy genesis_support_only_read on storage.objects
|
|
614
|
+
as restrictive for select to public
|
|
615
|
+
using (bucket_id <> 'genesis-support' or public.support_can_read(name, created_at));
|
|
616
|
+
create policy genesis_support_only_upload on storage.objects
|
|
617
|
+
as restrictive for insert to public
|
|
618
|
+
with check (bucket_id <> 'genesis-support' or (select public.support_can_upload(name)));
|
|
619
|
+
create policy genesis_support_no_update on storage.objects
|
|
620
|
+
as restrictive for update to public
|
|
621
|
+
using (bucket_id <> 'genesis-support')
|
|
622
|
+
with check (bucket_id <> 'genesis-support');
|
|
623
|
+
create policy genesis_support_no_delete on storage.objects
|
|
624
|
+
as restrictive for delete to public
|
|
625
|
+
using (bucket_id <> 'genesis-support');
|
|
626
|
+
|
|
627
|
+
-- Functions ---------------------------------------------------------------------------
|
|
628
|
+
|
|
629
|
+
-- Reading a thread: the person (on their own report) or staff mark it read.
|
|
630
|
+
create or replace function public.support_mark_read(report uuid)
|
|
631
|
+
returns void
|
|
632
|
+
language plpgsql
|
|
633
|
+
security definer
|
|
634
|
+
set search_path = ''
|
|
635
|
+
as $$
|
|
636
|
+
declare
|
|
637
|
+
me uuid := auth.uid();
|
|
638
|
+
owner uuid;
|
|
639
|
+
begin
|
|
640
|
+
select r.user_id into owner from public.support_reports r where r.id = report;
|
|
641
|
+
if me is null or owner is null then
|
|
642
|
+
raise exception 'support_mark_read: no such report' using errcode = '42501';
|
|
643
|
+
end if;
|
|
644
|
+
if owner = me then
|
|
645
|
+
update public.support_reports set user_read_at = clock_timestamp() where id = report;
|
|
646
|
+
elsif public.support_is_staff() then
|
|
647
|
+
update public.support_reports set staff_read_at = clock_timestamp() where id = report;
|
|
648
|
+
else
|
|
649
|
+
raise exception 'support_mark_read: no such report' using errcode = '42501';
|
|
650
|
+
end if;
|
|
651
|
+
end;
|
|
652
|
+
$$;
|
|
653
|
+
|
|
654
|
+
comment on function public.support_mark_read(uuid) is
|
|
655
|
+
'Marks a report''s thread read for its person, or for staff. Genesis support.';
|
|
656
|
+
|
|
657
|
+
-- Staff: a report's status, and the FeatureId it is about (null keeps it, '' clears it).
|
|
658
|
+
create or replace function public.support_set_report_status(report uuid, status text, feature text default null)
|
|
659
|
+
returns void
|
|
660
|
+
language plpgsql
|
|
661
|
+
security definer
|
|
662
|
+
set search_path = ''
|
|
663
|
+
as $$
|
|
664
|
+
begin
|
|
665
|
+
if not public.support_is_staff() then
|
|
666
|
+
raise exception 'support_set_report_status: only staff' using errcode = '42501';
|
|
667
|
+
end if;
|
|
668
|
+
update public.support_reports r
|
|
669
|
+
set status = support_set_report_status.status,
|
|
670
|
+
feature_id = case when feature = '' then null else coalesce(feature, r.feature_id) end
|
|
671
|
+
where r.id = report;
|
|
672
|
+
if not found then
|
|
673
|
+
raise exception 'support_set_report_status: no such report' using errcode = 'P0002';
|
|
674
|
+
end if;
|
|
675
|
+
end;
|
|
676
|
+
$$;
|
|
677
|
+
|
|
678
|
+
comment on function public.support_set_report_status(uuid, text, text) is
|
|
679
|
+
'Staff set a report''s status and the FeatureId it is about. Genesis support.';
|
|
680
|
+
|
|
681
|
+
-- Staff: an idea's status (shipped stamps shipped_at), and the FeatureId that delivers it
|
|
682
|
+
-- (null keeps it, '' clears it).
|
|
683
|
+
create or replace function public.support_set_idea_status(idea uuid, status text, feature text default null)
|
|
684
|
+
returns void
|
|
685
|
+
language plpgsql
|
|
686
|
+
security definer
|
|
687
|
+
set search_path = ''
|
|
688
|
+
as $$
|
|
689
|
+
begin
|
|
690
|
+
if not public.support_is_staff() then
|
|
691
|
+
raise exception 'support_set_idea_status: only staff' using errcode = '42501';
|
|
692
|
+
end if;
|
|
693
|
+
update public.support_ideas i
|
|
694
|
+
set status = support_set_idea_status.status,
|
|
695
|
+
feature_id = case when feature = '' then null else coalesce(feature, i.feature_id) end,
|
|
696
|
+
shipped_at = case when support_set_idea_status.status = 'shipped' then coalesce(i.shipped_at, now()) end
|
|
697
|
+
where i.id = idea;
|
|
698
|
+
if not found then
|
|
699
|
+
raise exception 'support_set_idea_status: no such idea' using errcode = 'P0002';
|
|
700
|
+
end if;
|
|
701
|
+
end;
|
|
702
|
+
$$;
|
|
703
|
+
|
|
704
|
+
comment on function public.support_set_idea_status(uuid, text, text) is
|
|
705
|
+
'Staff set an idea''s status (shipped stamps shipped_at) and the FeatureId that delivers it. Genesis support.';
|
|
706
|
+
|
|
707
|
+
-- "You asked, we shipped": shipped ideas the caller suggested or voted for, newest first.
|
|
708
|
+
create or replace function public.support_shipped_for_me(max_rows integer default 50)
|
|
709
|
+
returns table (id uuid, title text, body text, status text, feature_id text, shipped_at timestamptz, vote_count integer, created_at timestamptz, mine boolean, voted boolean)
|
|
710
|
+
language sql
|
|
711
|
+
stable
|
|
712
|
+
security definer
|
|
713
|
+
set search_path = ''
|
|
714
|
+
as $$
|
|
715
|
+
select i.id, i.title, i.body, i.status, i.feature_id, i.shipped_at, i.vote_count, i.created_at,
|
|
716
|
+
i.author_id = (select auth.uid()),
|
|
717
|
+
exists (select 1 from public.support_idea_votes v where v.idea_id = i.id and v.user_id = (select auth.uid()))
|
|
718
|
+
from public.support_ideas i
|
|
719
|
+
where i.status = 'shipped'
|
|
720
|
+
and (i.author_id = (select auth.uid())
|
|
721
|
+
or exists (select 1 from public.support_idea_votes v where v.idea_id = i.id and v.user_id = (select auth.uid())))
|
|
722
|
+
order by i.shipped_at desc nulls last, i.id
|
|
723
|
+
limit greatest(1, least(coalesce(max_rows, 50), 200))
|
|
724
|
+
$$;
|
|
725
|
+
|
|
726
|
+
comment on function public.support_shipped_for_me(integer) is
|
|
727
|
+
'Shipped ideas the caller suggested or voted for, with their FeatureIds. Genesis support.';
|
|
728
|
+
|
|
729
|
+
-- The ideas the caller suggested (who suggested an idea isn't readable by others).
|
|
730
|
+
create or replace function public.support_my_idea_ids()
|
|
731
|
+
returns setof uuid
|
|
732
|
+
language sql
|
|
733
|
+
stable
|
|
734
|
+
security definer
|
|
735
|
+
set search_path = ''
|
|
736
|
+
as $$
|
|
737
|
+
select i.id from public.support_ideas i where i.author_id = (select auth.uid())
|
|
738
|
+
$$;
|
|
739
|
+
|
|
740
|
+
comment on function public.support_my_idea_ids() is
|
|
741
|
+
'The ids of the ideas the caller suggested. Genesis support.';
|
|
742
|
+
|
|
743
|
+
-- For the cleanup function: objects in the bucket no attachment names, older than
|
|
744
|
+
-- `older_than` (an upload whose attachment row never landed). Service role only.
|
|
745
|
+
create or replace function public.support_orphan_objects(older_than interval default interval '1 day', max_rows integer default 500)
|
|
746
|
+
returns table (path text)
|
|
747
|
+
language sql
|
|
748
|
+
stable
|
|
749
|
+
security definer
|
|
750
|
+
set search_path = ''
|
|
751
|
+
as $$
|
|
752
|
+
select o.name from storage.objects o
|
|
753
|
+
where o.bucket_id = 'genesis-support'
|
|
754
|
+
and o.created_at <= now() - older_than
|
|
755
|
+
and not exists (select 1 from public.support_attachments a where a.path = o.name)
|
|
756
|
+
order by o.created_at
|
|
757
|
+
limit greatest(1, least(coalesce(max_rows, 500), 1000))
|
|
758
|
+
$$;
|
|
759
|
+
|
|
760
|
+
comment on function public.support_orphan_objects(interval, integer) is
|
|
761
|
+
'Screenshots uploaded but never recorded, for the cleanup function. Service role only. Genesis support.';
|
|
762
|
+
|
|
763
|
+
-- Privileges by name ------------------------------------------------------------------
|
|
764
|
+
-- Supabase's default privileges give anon and authenticated everything on a new public
|
|
765
|
+
-- table and function, and Postgres gives every function to public: take them back, then
|
|
766
|
+
-- grant what's needed, column by column where a column is the server's to set.
|
|
767
|
+
|
|
768
|
+
revoke all on table public.support_reports from public, anon, authenticated;
|
|
769
|
+
grant select on table public.support_reports to authenticated;
|
|
770
|
+
grant insert (type, title, message, route, app_version, platform, device, context) on table public.support_reports to authenticated;
|
|
771
|
+
grant update (type, title, message) on table public.support_reports to authenticated;
|
|
772
|
+
grant delete on table public.support_reports to authenticated;
|
|
773
|
+
|
|
774
|
+
revoke all on table public.support_attachments from public, anon, authenticated;
|
|
775
|
+
grant select on table public.support_attachments to authenticated;
|
|
776
|
+
grant insert (report_id, path, content_type, byte_size, width, height) on table public.support_attachments to authenticated;
|
|
777
|
+
|
|
778
|
+
revoke all on table public.support_attachment_purges from public, anon, authenticated;
|
|
779
|
+
revoke all on sequence public.support_attachment_purges_id_seq from public, anon, authenticated;
|
|
780
|
+
|
|
781
|
+
revoke all on table public.support_replies from public, anon, authenticated;
|
|
782
|
+
-- Not author_id: a person never learns staff members' ids (from_staff says who wrote it).
|
|
783
|
+
grant select (id, report_id, from_staff, body, created_at) on table public.support_replies to authenticated;
|
|
784
|
+
grant insert (report_id, body) on table public.support_replies to authenticated;
|
|
785
|
+
|
|
786
|
+
revoke all on table public.support_ideas from public, anon, authenticated;
|
|
787
|
+
-- Not author_id: ideas aren't linked to who suggested them (support_my_idea_ids answers "mine").
|
|
788
|
+
grant select (id, title, body, status, feature_id, shipped_at, vote_count, created_at, updated_at) on table public.support_ideas to authenticated;
|
|
789
|
+
grant insert (title, body) on table public.support_ideas to authenticated;
|
|
790
|
+
grant update (title, body) on table public.support_ideas to authenticated;
|
|
791
|
+
|
|
792
|
+
revoke all on table public.support_idea_votes from public, anon, authenticated;
|
|
793
|
+
grant select on table public.support_idea_votes to authenticated;
|
|
794
|
+
grant insert (idea_id) on table public.support_idea_votes to authenticated;
|
|
795
|
+
grant delete on table public.support_idea_votes to authenticated;
|
|
796
|
+
|
|
797
|
+
-- The service role (server jobs: the cleanup and notify functions, a staff tool). No
|
|
798
|
+
-- truncate (it would skip the triggers), references or trigger.
|
|
799
|
+
revoke all on table public.support_reports from service_role;
|
|
800
|
+
grant select, insert, update, delete on table public.support_reports to service_role;
|
|
801
|
+
revoke all on table public.support_attachments from service_role;
|
|
802
|
+
grant select, insert, update, delete on table public.support_attachments to service_role;
|
|
803
|
+
revoke all on table public.support_attachment_purges from service_role;
|
|
804
|
+
grant select, delete on table public.support_attachment_purges to service_role;
|
|
805
|
+
revoke all on table public.support_replies from service_role;
|
|
806
|
+
grant select, insert, update, delete on table public.support_replies to service_role;
|
|
807
|
+
revoke all on table public.support_ideas from service_role;
|
|
808
|
+
grant select, insert, update, delete on table public.support_ideas to service_role;
|
|
809
|
+
revoke all on table public.support_idea_votes from service_role;
|
|
810
|
+
grant select, insert, delete on table public.support_idea_votes to service_role;
|
|
811
|
+
|
|
812
|
+
revoke execute on function public.support_is_staff() from public, anon;
|
|
813
|
+
grant execute on function public.support_is_staff() to authenticated, service_role;
|
|
814
|
+
revoke execute on function public.support_mark_read(uuid) from public, anon;
|
|
815
|
+
grant execute on function public.support_mark_read(uuid) to authenticated;
|
|
816
|
+
revoke execute on function public.support_set_report_status(uuid, text, text) from public, anon;
|
|
817
|
+
grant execute on function public.support_set_report_status(uuid, text, text) to authenticated;
|
|
818
|
+
revoke execute on function public.support_set_idea_status(uuid, text, text) from public, anon;
|
|
819
|
+
grant execute on function public.support_set_idea_status(uuid, text, text) to authenticated;
|
|
820
|
+
revoke execute on function public.support_shipped_for_me(integer) from public, anon;
|
|
821
|
+
grant execute on function public.support_shipped_for_me(integer) to authenticated;
|
|
822
|
+
revoke execute on function public.support_my_idea_ids() from public, anon;
|
|
823
|
+
grant execute on function public.support_my_idea_ids() to authenticated;
|
|
824
|
+
revoke execute on function public.support_is_anonymous() from public, anon;
|
|
825
|
+
grant execute on function public.support_is_anonymous() to authenticated;
|
|
826
|
+
-- The storage policies call these for every role they hold, anon included; each answers
|
|
827
|
+
-- false without a session.
|
|
828
|
+
revoke execute on function public.support_can_upload(text) from public;
|
|
829
|
+
grant execute on function public.support_can_upload(text) to anon, authenticated;
|
|
830
|
+
revoke execute on function public.support_can_read(text, timestamptz) from public;
|
|
831
|
+
grant execute on function public.support_can_read(text, timestamptz) to anon, authenticated;
|
|
832
|
+
revoke execute on function public.support_orphan_objects(interval, integer) from public, anon, authenticated;
|
|
833
|
+
grant execute on function public.support_orphan_objects(interval, integer) to service_role;
|
|
834
|
+
|
|
835
|
+
revoke all on table private.support_rate_events from public, anon, authenticated;
|
|
836
|
+
|
|
837
|
+
-- Only the triggers above call these.
|
|
838
|
+
revoke execute on function private.genesis_support_rate_limit(uuid, text, integer, interval) from public, anon, authenticated;
|
|
839
|
+
revoke execute on function private.genesis_support_touch() from public, anon, authenticated;
|
|
840
|
+
revoke execute on function private.genesis_support_new_report() from public, anon, authenticated;
|
|
841
|
+
revoke execute on function private.genesis_support_new_attachment() from public, anon, authenticated;
|
|
842
|
+
revoke execute on function private.genesis_support_queue_purge() from public, anon, authenticated;
|
|
843
|
+
revoke execute on function private.genesis_support_new_reply() from public, anon, authenticated;
|
|
844
|
+
revoke execute on function private.genesis_support_reply_posted() from public, anon, authenticated;
|
|
845
|
+
revoke execute on function private.genesis_support_new_idea() from public, anon, authenticated;
|
|
846
|
+
revoke execute on function private.genesis_support_count_vote() from public, anon, authenticated;
|