create-bw-app 0.9.5 → 0.9.6
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 +2 -0
- package/package.json +1 -1
- package/src/generator.mjs +100 -36
- package/template/supabase/README.md +74 -0
- package/template/supabase/clients/README.md +19 -0
- package/template/supabase/module-registry.json +28 -0
- package/template/supabase/modules/admin/README.md +18 -0
- package/template/supabase/modules/admin/migrations/.gitkeep +1 -0
- package/template/supabase/modules/admin/migrations/20260316091000_admin_v1.sql +317 -0
- package/template/supabase/modules/core/README.md +24 -0
- package/template/supabase/modules/core/migrations/.gitkeep +1 -0
- package/template/supabase/modules/core/migrations/20260316090000_core_v1.sql +497 -0
- package/template/supabase/modules/crm/README.md +27 -0
- package/template/supabase/modules/crm/migrations/.gitkeep +1 -0
- package/template/supabase/modules/crm/migrations/20260316092000_crm_v1.sql +392 -0
- package/template/supabase/modules/projects/README.md +22 -0
- package/template/supabase/modules/projects/migrations/.gitkeep +1 -0
- package/template/supabase/modules/projects/migrations/20260316093000_projects_v1.sql +1120 -0
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
-- Brightweb core v1 baseline.
|
|
2
|
+
|
|
3
|
+
CREATE TABLE IF NOT EXISTS public.profiles (
|
|
4
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
5
|
+
email text NOT NULL,
|
|
6
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
7
|
+
first_name text,
|
|
8
|
+
last_name text,
|
|
9
|
+
user_id uuid UNIQUE REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
10
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
11
|
+
CONSTRAINT profiles_email_key UNIQUE (email)
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
CREATE INDEX IF NOT EXISTS idx_profiles_user_id
|
|
15
|
+
ON public.profiles (user_id);
|
|
16
|
+
|
|
17
|
+
CREATE OR REPLACE FUNCTION public.touch_profiles_updated_at()
|
|
18
|
+
RETURNS trigger
|
|
19
|
+
LANGUAGE plpgsql
|
|
20
|
+
SECURITY DEFINER
|
|
21
|
+
SET search_path = public
|
|
22
|
+
AS $$
|
|
23
|
+
BEGIN
|
|
24
|
+
NEW.updated_at := now();
|
|
25
|
+
RETURN NEW;
|
|
26
|
+
END;
|
|
27
|
+
$$;
|
|
28
|
+
|
|
29
|
+
DROP TRIGGER IF EXISTS trg_touch_profiles_updated_at ON public.profiles;
|
|
30
|
+
CREATE TRIGGER trg_touch_profiles_updated_at
|
|
31
|
+
BEFORE UPDATE ON public.profiles
|
|
32
|
+
FOR EACH ROW
|
|
33
|
+
EXECUTE FUNCTION public.touch_profiles_updated_at();
|
|
34
|
+
|
|
35
|
+
CREATE OR REPLACE FUNCTION public.current_profile_id()
|
|
36
|
+
RETURNS uuid
|
|
37
|
+
LANGUAGE sql
|
|
38
|
+
STABLE
|
|
39
|
+
SECURITY DEFINER
|
|
40
|
+
SET search_path = public
|
|
41
|
+
AS $$
|
|
42
|
+
SELECT p.id
|
|
43
|
+
FROM public.profiles p
|
|
44
|
+
WHERE p.user_id = auth.uid()
|
|
45
|
+
LIMIT 1
|
|
46
|
+
$$;
|
|
47
|
+
|
|
48
|
+
CREATE OR REPLACE FUNCTION public.current_global_role()
|
|
49
|
+
RETURNS text
|
|
50
|
+
LANGUAGE sql
|
|
51
|
+
STABLE
|
|
52
|
+
SECURITY DEFINER
|
|
53
|
+
SET search_path = public
|
|
54
|
+
AS $$
|
|
55
|
+
SELECT NULL::text
|
|
56
|
+
$$;
|
|
57
|
+
|
|
58
|
+
CREATE OR REPLACE FUNCTION public.has_role(target_role text)
|
|
59
|
+
RETURNS boolean
|
|
60
|
+
LANGUAGE sql
|
|
61
|
+
STABLE
|
|
62
|
+
SECURITY DEFINER
|
|
63
|
+
SET search_path = public
|
|
64
|
+
AS $$
|
|
65
|
+
SELECT false
|
|
66
|
+
$$;
|
|
67
|
+
|
|
68
|
+
CREATE OR REPLACE FUNCTION public.is_staff()
|
|
69
|
+
RETURNS boolean
|
|
70
|
+
LANGUAGE sql
|
|
71
|
+
STABLE
|
|
72
|
+
SECURITY DEFINER
|
|
73
|
+
SET search_path = public
|
|
74
|
+
AS $$
|
|
75
|
+
SELECT false
|
|
76
|
+
$$;
|
|
77
|
+
|
|
78
|
+
CREATE OR REPLACE FUNCTION public.is_admin()
|
|
79
|
+
RETURNS boolean
|
|
80
|
+
LANGUAGE sql
|
|
81
|
+
STABLE
|
|
82
|
+
SECURITY DEFINER
|
|
83
|
+
SET search_path = public
|
|
84
|
+
AS $$
|
|
85
|
+
SELECT false
|
|
86
|
+
$$;
|
|
87
|
+
|
|
88
|
+
ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;
|
|
89
|
+
|
|
90
|
+
DROP POLICY IF EXISTS "Authenticated users can insert own profile" ON public.profiles;
|
|
91
|
+
CREATE POLICY "Authenticated users can insert own profile"
|
|
92
|
+
ON public.profiles
|
|
93
|
+
FOR INSERT
|
|
94
|
+
TO authenticated
|
|
95
|
+
WITH CHECK (auth.uid() = user_id);
|
|
96
|
+
|
|
97
|
+
DROP POLICY IF EXISTS "Authenticated users can update own profile" ON public.profiles;
|
|
98
|
+
CREATE POLICY "Authenticated users can update own profile"
|
|
99
|
+
ON public.profiles
|
|
100
|
+
FOR UPDATE
|
|
101
|
+
TO authenticated
|
|
102
|
+
USING (auth.uid() = user_id)
|
|
103
|
+
WITH CHECK (auth.uid() = user_id);
|
|
104
|
+
|
|
105
|
+
DROP POLICY IF EXISTS "Authenticated users can view own profile" ON public.profiles;
|
|
106
|
+
CREATE POLICY "Authenticated users can view own profile"
|
|
107
|
+
ON public.profiles
|
|
108
|
+
FOR SELECT
|
|
109
|
+
TO authenticated
|
|
110
|
+
USING (auth.uid() = user_id);
|
|
111
|
+
|
|
112
|
+
DROP POLICY IF EXISTS "Service role full access to profiles" ON public.profiles;
|
|
113
|
+
CREATE POLICY "Service role full access to profiles"
|
|
114
|
+
ON public.profiles
|
|
115
|
+
TO service_role
|
|
116
|
+
USING (true)
|
|
117
|
+
WITH CHECK (true);
|
|
118
|
+
|
|
119
|
+
CREATE OR REPLACE FUNCTION public.sync_profile_from_auth_identity(
|
|
120
|
+
p_user_id uuid,
|
|
121
|
+
p_email text,
|
|
122
|
+
p_metadata jsonb DEFAULT '{}'::jsonb
|
|
123
|
+
)
|
|
124
|
+
RETURNS void
|
|
125
|
+
LANGUAGE plpgsql
|
|
126
|
+
SECURITY DEFINER
|
|
127
|
+
SET search_path = public, auth
|
|
128
|
+
AS $$
|
|
129
|
+
DECLARE
|
|
130
|
+
v_email text;
|
|
131
|
+
v_first_name text;
|
|
132
|
+
v_last_name text;
|
|
133
|
+
v_full_name text;
|
|
134
|
+
v_name_parts text[];
|
|
135
|
+
BEGIN
|
|
136
|
+
v_email := NULLIF(lower(trim(COALESCE(p_email, ''))), '');
|
|
137
|
+
|
|
138
|
+
IF v_email IS NULL THEN
|
|
139
|
+
RETURN;
|
|
140
|
+
END IF;
|
|
141
|
+
|
|
142
|
+
v_first_name := NULLIF(trim(COALESCE(p_metadata ->> 'first_name', '')), '');
|
|
143
|
+
v_last_name := NULLIF(trim(COALESCE(p_metadata ->> 'last_name', '')), '');
|
|
144
|
+
v_full_name := NULLIF(
|
|
145
|
+
trim(
|
|
146
|
+
COALESCE(
|
|
147
|
+
p_metadata ->> 'full_name',
|
|
148
|
+
p_metadata ->> 'name',
|
|
149
|
+
p_metadata ->> 'display_name',
|
|
150
|
+
''
|
|
151
|
+
)
|
|
152
|
+
),
|
|
153
|
+
''
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
IF v_first_name IS NULL AND v_full_name IS NOT NULL THEN
|
|
157
|
+
v_name_parts := regexp_split_to_array(v_full_name, '\s+');
|
|
158
|
+
IF array_length(v_name_parts, 1) >= 1 THEN
|
|
159
|
+
v_first_name := v_name_parts[1];
|
|
160
|
+
END IF;
|
|
161
|
+
IF array_length(v_name_parts, 1) >= 2 THEN
|
|
162
|
+
v_last_name := array_to_string(v_name_parts[2:array_length(v_name_parts, 1)], ' ');
|
|
163
|
+
END IF;
|
|
164
|
+
END IF;
|
|
165
|
+
|
|
166
|
+
INSERT INTO public.profiles (
|
|
167
|
+
user_id,
|
|
168
|
+
email,
|
|
169
|
+
first_name,
|
|
170
|
+
last_name,
|
|
171
|
+
created_at,
|
|
172
|
+
updated_at
|
|
173
|
+
)
|
|
174
|
+
VALUES (
|
|
175
|
+
p_user_id,
|
|
176
|
+
v_email,
|
|
177
|
+
v_first_name,
|
|
178
|
+
v_last_name,
|
|
179
|
+
now(),
|
|
180
|
+
now()
|
|
181
|
+
)
|
|
182
|
+
ON CONFLICT (user_id) DO UPDATE
|
|
183
|
+
SET
|
|
184
|
+
email = EXCLUDED.email,
|
|
185
|
+
first_name = COALESCE(NULLIF(trim(public.profiles.first_name), ''), EXCLUDED.first_name),
|
|
186
|
+
last_name = COALESCE(NULLIF(trim(public.profiles.last_name), ''), EXCLUDED.last_name),
|
|
187
|
+
updated_at = now();
|
|
188
|
+
END;
|
|
189
|
+
$$;
|
|
190
|
+
|
|
191
|
+
CREATE OR REPLACE FUNCTION public.handle_auth_user_profile_sync()
|
|
192
|
+
RETURNS trigger
|
|
193
|
+
LANGUAGE plpgsql
|
|
194
|
+
SECURITY DEFINER
|
|
195
|
+
SET search_path = public, auth
|
|
196
|
+
AS $$
|
|
197
|
+
BEGIN
|
|
198
|
+
BEGIN
|
|
199
|
+
PERFORM public.sync_profile_from_auth_identity(NEW.id, NEW.email, NEW.raw_user_meta_data);
|
|
200
|
+
EXCEPTION
|
|
201
|
+
WHEN OTHERS THEN
|
|
202
|
+
RAISE WARNING 'handle_auth_user_profile_sync failed for user %: %', NEW.id, SQLERRM;
|
|
203
|
+
END;
|
|
204
|
+
|
|
205
|
+
RETURN NEW;
|
|
206
|
+
END;
|
|
207
|
+
$$;
|
|
208
|
+
|
|
209
|
+
DROP TRIGGER IF EXISTS on_auth_user_profile_sync ON auth.users;
|
|
210
|
+
CREATE TRIGGER on_auth_user_profile_sync
|
|
211
|
+
AFTER INSERT OR UPDATE OF email, raw_user_meta_data
|
|
212
|
+
ON auth.users
|
|
213
|
+
FOR EACH ROW
|
|
214
|
+
EXECUTE FUNCTION public.handle_auth_user_profile_sync();
|
|
215
|
+
|
|
216
|
+
DO $$
|
|
217
|
+
DECLARE
|
|
218
|
+
auth_user record;
|
|
219
|
+
BEGIN
|
|
220
|
+
FOR auth_user IN
|
|
221
|
+
SELECT id, email, raw_user_meta_data
|
|
222
|
+
FROM auth.users
|
|
223
|
+
LOOP
|
|
224
|
+
PERFORM public.sync_profile_from_auth_identity(auth_user.id, auth_user.email, auth_user.raw_user_meta_data);
|
|
225
|
+
END LOOP;
|
|
226
|
+
END;
|
|
227
|
+
$$;
|
|
228
|
+
|
|
229
|
+
CREATE TABLE IF NOT EXISTS public.user_preferences (
|
|
230
|
+
profile_id uuid PRIMARY KEY REFERENCES public.profiles(id) ON DELETE CASCADE,
|
|
231
|
+
preferred_language text NOT NULL DEFAULT 'pt-PT',
|
|
232
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
233
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
234
|
+
CONSTRAINT user_preferences_preferred_language_check
|
|
235
|
+
CHECK (preferred_language IN ('pt-PT', 'en'))
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
CREATE OR REPLACE FUNCTION public.touch_user_preferences_updated_at()
|
|
239
|
+
RETURNS trigger
|
|
240
|
+
LANGUAGE plpgsql
|
|
241
|
+
SECURITY DEFINER
|
|
242
|
+
SET search_path = public
|
|
243
|
+
AS $$
|
|
244
|
+
BEGIN
|
|
245
|
+
NEW.updated_at := now();
|
|
246
|
+
RETURN NEW;
|
|
247
|
+
END;
|
|
248
|
+
$$;
|
|
249
|
+
|
|
250
|
+
DROP TRIGGER IF EXISTS trg_touch_user_preferences_updated_at ON public.user_preferences;
|
|
251
|
+
CREATE TRIGGER trg_touch_user_preferences_updated_at
|
|
252
|
+
BEFORE UPDATE ON public.user_preferences
|
|
253
|
+
FOR EACH ROW
|
|
254
|
+
EXECUTE FUNCTION public.touch_user_preferences_updated_at();
|
|
255
|
+
|
|
256
|
+
ALTER TABLE public.user_preferences ENABLE ROW LEVEL SECURITY;
|
|
257
|
+
|
|
258
|
+
DROP POLICY IF EXISTS "Users view own preferences" ON public.user_preferences;
|
|
259
|
+
CREATE POLICY "Users view own preferences"
|
|
260
|
+
ON public.user_preferences
|
|
261
|
+
FOR SELECT
|
|
262
|
+
TO authenticated
|
|
263
|
+
USING (
|
|
264
|
+
profile_id = public.current_profile_id()
|
|
265
|
+
OR public.is_staff()
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
DROP POLICY IF EXISTS "Users insert own preferences" ON public.user_preferences;
|
|
269
|
+
CREATE POLICY "Users insert own preferences"
|
|
270
|
+
ON public.user_preferences
|
|
271
|
+
FOR INSERT
|
|
272
|
+
TO authenticated
|
|
273
|
+
WITH CHECK (
|
|
274
|
+
profile_id = public.current_profile_id()
|
|
275
|
+
OR public.is_staff()
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
DROP POLICY IF EXISTS "Users update own preferences" ON public.user_preferences;
|
|
279
|
+
CREATE POLICY "Users update own preferences"
|
|
280
|
+
ON public.user_preferences
|
|
281
|
+
FOR UPDATE
|
|
282
|
+
TO authenticated
|
|
283
|
+
USING (
|
|
284
|
+
profile_id = public.current_profile_id()
|
|
285
|
+
OR public.is_staff()
|
|
286
|
+
)
|
|
287
|
+
WITH CHECK (
|
|
288
|
+
profile_id = public.current_profile_id()
|
|
289
|
+
OR public.is_staff()
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
INSERT INTO public.user_preferences (profile_id)
|
|
293
|
+
SELECT p.id
|
|
294
|
+
FROM public.profiles p
|
|
295
|
+
ON CONFLICT (profile_id) DO NOTHING;
|
|
296
|
+
|
|
297
|
+
CREATE TABLE IF NOT EXISTS public.user_notification_state (
|
|
298
|
+
profile_id uuid PRIMARY KEY REFERENCES public.profiles(id) ON DELETE CASCADE,
|
|
299
|
+
alerts_seen_at timestamptz,
|
|
300
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
301
|
+
updated_at timestamptz NOT NULL DEFAULT now()
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
CREATE OR REPLACE FUNCTION public.touch_user_notification_state_updated_at()
|
|
305
|
+
RETURNS trigger
|
|
306
|
+
LANGUAGE plpgsql
|
|
307
|
+
SECURITY DEFINER
|
|
308
|
+
SET search_path = public
|
|
309
|
+
AS $$
|
|
310
|
+
BEGIN
|
|
311
|
+
NEW.updated_at := now();
|
|
312
|
+
RETURN NEW;
|
|
313
|
+
END;
|
|
314
|
+
$$;
|
|
315
|
+
|
|
316
|
+
DROP TRIGGER IF EXISTS trg_touch_user_notification_state_updated_at ON public.user_notification_state;
|
|
317
|
+
CREATE TRIGGER trg_touch_user_notification_state_updated_at
|
|
318
|
+
BEFORE UPDATE ON public.user_notification_state
|
|
319
|
+
FOR EACH ROW
|
|
320
|
+
EXECUTE FUNCTION public.touch_user_notification_state_updated_at();
|
|
321
|
+
|
|
322
|
+
ALTER TABLE public.user_notification_state ENABLE ROW LEVEL SECURITY;
|
|
323
|
+
|
|
324
|
+
DROP POLICY IF EXISTS "Users view own notification state" ON public.user_notification_state;
|
|
325
|
+
CREATE POLICY "Users view own notification state"
|
|
326
|
+
ON public.user_notification_state
|
|
327
|
+
FOR SELECT
|
|
328
|
+
TO authenticated
|
|
329
|
+
USING (profile_id = public.current_profile_id());
|
|
330
|
+
|
|
331
|
+
DROP POLICY IF EXISTS "Users insert own notification state" ON public.user_notification_state;
|
|
332
|
+
CREATE POLICY "Users insert own notification state"
|
|
333
|
+
ON public.user_notification_state
|
|
334
|
+
FOR INSERT
|
|
335
|
+
TO authenticated
|
|
336
|
+
WITH CHECK (profile_id = public.current_profile_id());
|
|
337
|
+
|
|
338
|
+
DROP POLICY IF EXISTS "Users update own notification state" ON public.user_notification_state;
|
|
339
|
+
CREATE POLICY "Users update own notification state"
|
|
340
|
+
ON public.user_notification_state
|
|
341
|
+
FOR UPDATE
|
|
342
|
+
TO authenticated
|
|
343
|
+
USING (profile_id = public.current_profile_id())
|
|
344
|
+
WITH CHECK (profile_id = public.current_profile_id());
|
|
345
|
+
|
|
346
|
+
INSERT INTO public.user_notification_state (profile_id)
|
|
347
|
+
SELECT p.id
|
|
348
|
+
FROM public.profiles p
|
|
349
|
+
ON CONFLICT (profile_id) DO NOTHING;
|
|
350
|
+
|
|
351
|
+
CREATE TABLE IF NOT EXISTS public.app_activity_events (
|
|
352
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
353
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
354
|
+
domain text NOT NULL,
|
|
355
|
+
event_type text NOT NULL,
|
|
356
|
+
entity_table text NOT NULL,
|
|
357
|
+
entity_id uuid,
|
|
358
|
+
actor_profile_id uuid REFERENCES public.profiles(id) ON DELETE SET NULL,
|
|
359
|
+
summary text NOT NULL,
|
|
360
|
+
payload jsonb NOT NULL DEFAULT '{}'::jsonb
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
CREATE INDEX IF NOT EXISTS idx_app_activity_events_created_at_desc
|
|
364
|
+
ON public.app_activity_events (created_at DESC);
|
|
365
|
+
|
|
366
|
+
CREATE INDEX IF NOT EXISTS idx_app_activity_events_domain_created_at_desc
|
|
367
|
+
ON public.app_activity_events (domain, created_at DESC);
|
|
368
|
+
|
|
369
|
+
CREATE INDEX IF NOT EXISTS idx_app_activity_events_entity
|
|
370
|
+
ON public.app_activity_events (entity_table, entity_id);
|
|
371
|
+
|
|
372
|
+
CREATE INDEX IF NOT EXISTS idx_app_activity_events_actor_profile
|
|
373
|
+
ON public.app_activity_events (actor_profile_id);
|
|
374
|
+
|
|
375
|
+
ALTER TABLE public.app_activity_events ENABLE ROW LEVEL SECURITY;
|
|
376
|
+
|
|
377
|
+
DROP POLICY IF EXISTS "Staff can view app activity events" ON public.app_activity_events;
|
|
378
|
+
CREATE POLICY "Staff can view app activity events"
|
|
379
|
+
ON public.app_activity_events
|
|
380
|
+
FOR SELECT
|
|
381
|
+
TO authenticated
|
|
382
|
+
USING (public.is_staff());
|
|
383
|
+
|
|
384
|
+
CREATE OR REPLACE FUNCTION public.log_app_activity_event(
|
|
385
|
+
p_domain text,
|
|
386
|
+
p_event_type text,
|
|
387
|
+
p_entity_table text,
|
|
388
|
+
p_entity_id uuid,
|
|
389
|
+
p_summary text,
|
|
390
|
+
p_payload jsonb DEFAULT '{}'::jsonb,
|
|
391
|
+
p_actor_profile_id uuid DEFAULT NULL
|
|
392
|
+
)
|
|
393
|
+
RETURNS void
|
|
394
|
+
LANGUAGE plpgsql
|
|
395
|
+
SECURITY DEFINER
|
|
396
|
+
SET search_path = public
|
|
397
|
+
AS $$
|
|
398
|
+
DECLARE
|
|
399
|
+
v_actor_profile_id uuid;
|
|
400
|
+
BEGIN
|
|
401
|
+
v_actor_profile_id := COALESCE(p_actor_profile_id, public.current_profile_id());
|
|
402
|
+
|
|
403
|
+
INSERT INTO public.app_activity_events (
|
|
404
|
+
domain,
|
|
405
|
+
event_type,
|
|
406
|
+
entity_table,
|
|
407
|
+
entity_id,
|
|
408
|
+
actor_profile_id,
|
|
409
|
+
summary,
|
|
410
|
+
payload
|
|
411
|
+
)
|
|
412
|
+
VALUES (
|
|
413
|
+
p_domain,
|
|
414
|
+
p_event_type,
|
|
415
|
+
p_entity_table,
|
|
416
|
+
p_entity_id,
|
|
417
|
+
v_actor_profile_id,
|
|
418
|
+
p_summary,
|
|
419
|
+
COALESCE(p_payload, '{}'::jsonb)
|
|
420
|
+
);
|
|
421
|
+
END;
|
|
422
|
+
$$;
|
|
423
|
+
|
|
424
|
+
REVOKE ALL ON FUNCTION public.log_app_activity_event(text, text, text, uuid, text, jsonb, uuid)
|
|
425
|
+
FROM PUBLIC;
|
|
426
|
+
REVOKE ALL ON FUNCTION public.log_app_activity_event(text, text, text, uuid, text, jsonb, uuid)
|
|
427
|
+
FROM anon;
|
|
428
|
+
REVOKE ALL ON FUNCTION public.log_app_activity_event(text, text, text, uuid, text, jsonb, uuid)
|
|
429
|
+
FROM authenticated;
|
|
430
|
+
GRANT EXECUTE ON FUNCTION public.log_app_activity_event(text, text, text, uuid, text, jsonb, uuid)
|
|
431
|
+
TO service_role;
|
|
432
|
+
|
|
433
|
+
CREATE TABLE IF NOT EXISTS public.rate_limit_counters (
|
|
434
|
+
key text PRIMARY KEY,
|
|
435
|
+
window_started_at timestamptz NOT NULL,
|
|
436
|
+
count integer NOT NULL DEFAULT 1 CHECK (count >= 0),
|
|
437
|
+
updated_at timestamptz NOT NULL DEFAULT timezone('utc', now()),
|
|
438
|
+
expires_at timestamptz NOT NULL
|
|
439
|
+
);
|
|
440
|
+
|
|
441
|
+
CREATE INDEX IF NOT EXISTS idx_rate_limit_counters_expires_at
|
|
442
|
+
ON public.rate_limit_counters (expires_at);
|
|
443
|
+
|
|
444
|
+
REVOKE ALL ON public.rate_limit_counters FROM anon, authenticated;
|
|
445
|
+
|
|
446
|
+
CREATE OR REPLACE FUNCTION public.check_rate_limit(
|
|
447
|
+
p_key text,
|
|
448
|
+
p_window_seconds integer,
|
|
449
|
+
p_max_requests integer
|
|
450
|
+
)
|
|
451
|
+
RETURNS jsonb
|
|
452
|
+
LANGUAGE plpgsql
|
|
453
|
+
SECURITY DEFINER
|
|
454
|
+
SET search_path = public
|
|
455
|
+
AS $$
|
|
456
|
+
DECLARE
|
|
457
|
+
v_now timestamptz := timezone('utc', now());
|
|
458
|
+
v_window_seconds integer := GREATEST(1, p_window_seconds);
|
|
459
|
+
v_max_requests integer := GREATEST(1, p_max_requests);
|
|
460
|
+
v_window_start_epoch bigint;
|
|
461
|
+
v_window_start timestamptz;
|
|
462
|
+
v_window_interval interval;
|
|
463
|
+
v_count integer;
|
|
464
|
+
v_reset_at timestamptz;
|
|
465
|
+
v_allowed boolean;
|
|
466
|
+
BEGIN
|
|
467
|
+
DELETE FROM public.rate_limit_counters
|
|
468
|
+
WHERE expires_at < v_now;
|
|
469
|
+
|
|
470
|
+
v_window_start_epoch := FLOOR(EXTRACT(EPOCH FROM v_now) / v_window_seconds) * v_window_seconds;
|
|
471
|
+
v_window_start := to_timestamp(v_window_start_epoch);
|
|
472
|
+
v_window_interval := make_interval(secs => v_window_seconds);
|
|
473
|
+
|
|
474
|
+
INSERT INTO public.rate_limit_counters (key, window_started_at, count, updated_at, expires_at)
|
|
475
|
+
VALUES (p_key, v_window_start, 1, v_now, v_window_start + v_window_interval)
|
|
476
|
+
ON CONFLICT (key) DO UPDATE
|
|
477
|
+
SET count = CASE
|
|
478
|
+
WHEN public.rate_limit_counters.window_started_at = EXCLUDED.window_started_at
|
|
479
|
+
THEN public.rate_limit_counters.count + 1
|
|
480
|
+
ELSE 1
|
|
481
|
+
END,
|
|
482
|
+
window_started_at = EXCLUDED.window_started_at,
|
|
483
|
+
updated_at = v_now,
|
|
484
|
+
expires_at = EXCLUDED.expires_at
|
|
485
|
+
RETURNING count, (window_started_at + v_window_interval) INTO v_count, v_reset_at;
|
|
486
|
+
|
|
487
|
+
v_allowed := v_count <= v_max_requests;
|
|
488
|
+
|
|
489
|
+
RETURN jsonb_build_object(
|
|
490
|
+
'allowed', v_allowed,
|
|
491
|
+
'remaining', GREATEST(v_max_requests - v_count, 0),
|
|
492
|
+
'reset_at', v_reset_at
|
|
493
|
+
);
|
|
494
|
+
END;
|
|
495
|
+
$$;
|
|
496
|
+
|
|
497
|
+
GRANT EXECUTE ON FUNCTION public.check_rate_limit(text, integer, integer) TO anon, authenticated, service_role;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# CRM Migrations
|
|
2
|
+
|
|
3
|
+
`crm` owns organization, contact, and marketing-adjacent operational data.
|
|
4
|
+
|
|
5
|
+
## Owns
|
|
6
|
+
|
|
7
|
+
- `organizations`
|
|
8
|
+
- `crm_contacts`
|
|
9
|
+
- `crm_status_log`
|
|
10
|
+
- `organization_members`
|
|
11
|
+
- `organization_invitations`
|
|
12
|
+
- CRM/org helper functions like `is_org_admin()` and `set_crm_status(...)`
|
|
13
|
+
- CRM/org RLS and triggers
|
|
14
|
+
|
|
15
|
+
## Shared boundary
|
|
16
|
+
|
|
17
|
+
Some marketing data is adjacent to CRM, but the Brightweb v1 split is:
|
|
18
|
+
|
|
19
|
+
- CRM owns operational contact and organization management
|
|
20
|
+
- Marketing automation remains its own future module boundary
|
|
21
|
+
|
|
22
|
+
## Dependency
|
|
23
|
+
|
|
24
|
+
Depends on:
|
|
25
|
+
|
|
26
|
+
- `core`
|
|
27
|
+
- `admin` when staff/admin role helpers are referenced
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|