create-bw-app 0.9.4 → 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 +5 -0
- package/package.json +1 -1
- package/src/cli.mjs +5 -0
- package/src/constants.mjs +1 -0
- package/src/generator.mjs +102 -36
- package/src/update.mjs +67 -1
- package/template/base/AGENTS.md +1 -0
- package/template/base/app/playground/auth/page.tsx +1 -1
- package/template/base/app/preview/app-shell/page.tsx +1 -1
- package/template/base/{app/preview → components}/app-shell-preview.tsx +2 -2
- package/template/base/docs/ai/README.md +3 -0
- package/template/base/docs/ai/examples.md +2 -0
- package/template/modules/crm/app/api/crm/_shared/create-module-route-handler.ts +13 -0
- package/template/modules/crm/app/api/crm/contacts/route.ts +6 -4
- package/template/modules/crm/app/api/crm/organizations/route.ts +6 -4
- package/template/modules/crm/app/api/crm/owners/route.ts +6 -4
- package/template/modules/crm/app/api/crm/stats/route.ts +6 -4
- 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
- /package/template/base/{app/playground/auth → components}/auth-playground.tsx +0 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Admin Migrations
|
|
2
|
+
|
|
3
|
+
`admin` owns user governance, RBAC, and privileged role-management behavior.
|
|
4
|
+
|
|
5
|
+
## Owns
|
|
6
|
+
|
|
7
|
+
- `roles`
|
|
8
|
+
- `user_role_assignments`
|
|
9
|
+
- `role_change_audit`
|
|
10
|
+
- admin-only helper functions like `admin_set_user_role(...)`
|
|
11
|
+
- privileged profile-field guards
|
|
12
|
+
- default role-assignment triggers/backfills
|
|
13
|
+
|
|
14
|
+
## Dependency
|
|
15
|
+
|
|
16
|
+
Depends on:
|
|
17
|
+
|
|
18
|
+
- `core` (`profiles`, `current_profile_id()`, auth linkage)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
-- Brightweb admin v1 baseline.
|
|
2
|
+
|
|
3
|
+
CREATE TABLE IF NOT EXISTS public.roles (
|
|
4
|
+
code text PRIMARY KEY,
|
|
5
|
+
label text NOT NULL,
|
|
6
|
+
created_at timestamptz NOT NULL DEFAULT now()
|
|
7
|
+
);
|
|
8
|
+
|
|
9
|
+
ALTER TABLE public.roles ENABLE ROW LEVEL SECURITY;
|
|
10
|
+
|
|
11
|
+
DROP POLICY IF EXISTS "Authenticated users can view roles" ON public.roles;
|
|
12
|
+
CREATE POLICY "Authenticated users can view roles"
|
|
13
|
+
ON public.roles
|
|
14
|
+
FOR SELECT
|
|
15
|
+
TO authenticated
|
|
16
|
+
USING (true);
|
|
17
|
+
|
|
18
|
+
INSERT INTO public.roles (code, label)
|
|
19
|
+
VALUES
|
|
20
|
+
('client', 'Client'),
|
|
21
|
+
('staff', 'Staff'),
|
|
22
|
+
('admin', 'Admin')
|
|
23
|
+
ON CONFLICT (code) DO UPDATE
|
|
24
|
+
SET label = EXCLUDED.label;
|
|
25
|
+
|
|
26
|
+
CREATE TABLE IF NOT EXISTS public.user_role_assignments (
|
|
27
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
28
|
+
profile_id uuid NOT NULL UNIQUE REFERENCES public.profiles(id) ON DELETE CASCADE,
|
|
29
|
+
role_code text NOT NULL REFERENCES public.roles(code),
|
|
30
|
+
assigned_by_profile_id uuid REFERENCES public.profiles(id) ON DELETE SET NULL,
|
|
31
|
+
assigned_at timestamptz NOT NULL DEFAULT now(),
|
|
32
|
+
reason text
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
CREATE INDEX IF NOT EXISTS idx_user_role_assignments_role_code
|
|
36
|
+
ON public.user_role_assignments (role_code);
|
|
37
|
+
|
|
38
|
+
ALTER TABLE public.user_role_assignments ENABLE ROW LEVEL SECURITY;
|
|
39
|
+
|
|
40
|
+
DROP POLICY IF EXISTS "Users can view own role assignments" ON public.user_role_assignments;
|
|
41
|
+
CREATE POLICY "Users can view own role assignments"
|
|
42
|
+
ON public.user_role_assignments
|
|
43
|
+
FOR SELECT
|
|
44
|
+
TO authenticated
|
|
45
|
+
USING (profile_id = public.current_profile_id());
|
|
46
|
+
|
|
47
|
+
DROP POLICY IF EXISTS "Staff can view role assignments" ON public.user_role_assignments;
|
|
48
|
+
CREATE POLICY "Staff can view role assignments"
|
|
49
|
+
ON public.user_role_assignments
|
|
50
|
+
FOR SELECT
|
|
51
|
+
TO authenticated
|
|
52
|
+
USING (public.is_staff());
|
|
53
|
+
|
|
54
|
+
CREATE TABLE IF NOT EXISTS public.role_change_audit (
|
|
55
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
56
|
+
target_profile_id uuid NOT NULL REFERENCES public.profiles(id) ON DELETE CASCADE,
|
|
57
|
+
changed_by_profile_id uuid NOT NULL REFERENCES public.profiles(id) ON DELETE RESTRICT,
|
|
58
|
+
old_role_code text,
|
|
59
|
+
new_role_code text NOT NULL REFERENCES public.roles(code),
|
|
60
|
+
reason text,
|
|
61
|
+
created_at timestamptz NOT NULL DEFAULT now()
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
CREATE INDEX IF NOT EXISTS idx_role_change_audit_target
|
|
65
|
+
ON public.role_change_audit (target_profile_id, created_at DESC);
|
|
66
|
+
|
|
67
|
+
ALTER TABLE public.role_change_audit ENABLE ROW LEVEL SECURITY;
|
|
68
|
+
|
|
69
|
+
DROP POLICY IF EXISTS "Admins can view role change audit" ON public.role_change_audit;
|
|
70
|
+
CREATE POLICY "Admins can view role change audit"
|
|
71
|
+
ON public.role_change_audit
|
|
72
|
+
FOR SELECT
|
|
73
|
+
TO authenticated
|
|
74
|
+
USING (public.is_admin());
|
|
75
|
+
|
|
76
|
+
CREATE OR REPLACE FUNCTION public.current_global_role()
|
|
77
|
+
RETURNS text
|
|
78
|
+
LANGUAGE sql
|
|
79
|
+
STABLE
|
|
80
|
+
SECURITY DEFINER
|
|
81
|
+
SET search_path = public
|
|
82
|
+
AS $$
|
|
83
|
+
SELECT ura.role_code
|
|
84
|
+
FROM public.user_role_assignments ura
|
|
85
|
+
WHERE ura.profile_id = public.current_profile_id()
|
|
86
|
+
LIMIT 1
|
|
87
|
+
$$;
|
|
88
|
+
|
|
89
|
+
CREATE OR REPLACE FUNCTION public.has_role(target_role text)
|
|
90
|
+
RETURNS boolean
|
|
91
|
+
LANGUAGE sql
|
|
92
|
+
STABLE
|
|
93
|
+
SECURITY DEFINER
|
|
94
|
+
SET search_path = public
|
|
95
|
+
AS $$
|
|
96
|
+
SELECT EXISTS (
|
|
97
|
+
SELECT 1
|
|
98
|
+
FROM public.user_role_assignments ura
|
|
99
|
+
WHERE ura.profile_id = public.current_profile_id()
|
|
100
|
+
AND ura.role_code = target_role
|
|
101
|
+
)
|
|
102
|
+
$$;
|
|
103
|
+
|
|
104
|
+
CREATE OR REPLACE FUNCTION public.is_staff()
|
|
105
|
+
RETURNS boolean
|
|
106
|
+
LANGUAGE sql
|
|
107
|
+
STABLE
|
|
108
|
+
SECURITY DEFINER
|
|
109
|
+
SET search_path = public
|
|
110
|
+
AS $$
|
|
111
|
+
SELECT COALESCE(public.current_global_role() IN ('staff', 'admin'), false)
|
|
112
|
+
$$;
|
|
113
|
+
|
|
114
|
+
CREATE OR REPLACE FUNCTION public.is_admin()
|
|
115
|
+
RETURNS boolean
|
|
116
|
+
LANGUAGE sql
|
|
117
|
+
STABLE
|
|
118
|
+
SECURITY DEFINER
|
|
119
|
+
SET search_path = public
|
|
120
|
+
AS $$
|
|
121
|
+
SELECT COALESCE(public.current_global_role() = 'admin', false)
|
|
122
|
+
$$;
|
|
123
|
+
|
|
124
|
+
CREATE OR REPLACE FUNCTION public.admin_set_user_role(
|
|
125
|
+
p_target_profile_id uuid,
|
|
126
|
+
p_new_role_code text,
|
|
127
|
+
p_reason text DEFAULT NULL
|
|
128
|
+
)
|
|
129
|
+
RETURNS TABLE (
|
|
130
|
+
changed boolean,
|
|
131
|
+
reason text,
|
|
132
|
+
old_role_code text,
|
|
133
|
+
new_role_code text
|
|
134
|
+
)
|
|
135
|
+
LANGUAGE plpgsql
|
|
136
|
+
SECURITY DEFINER
|
|
137
|
+
SET search_path = public
|
|
138
|
+
AS $$
|
|
139
|
+
DECLARE
|
|
140
|
+
v_actor_profile_id uuid;
|
|
141
|
+
v_target_exists boolean;
|
|
142
|
+
v_old_role text;
|
|
143
|
+
v_admin_count integer;
|
|
144
|
+
BEGIN
|
|
145
|
+
v_actor_profile_id := public.current_profile_id();
|
|
146
|
+
|
|
147
|
+
IF v_actor_profile_id IS NULL OR NOT public.is_admin() THEN
|
|
148
|
+
RAISE EXCEPTION 'Only admins can change roles'
|
|
149
|
+
USING ERRCODE = '42501';
|
|
150
|
+
END IF;
|
|
151
|
+
|
|
152
|
+
IF p_new_role_code IS NULL OR NOT EXISTS (
|
|
153
|
+
SELECT 1 FROM public.roles r WHERE r.code = p_new_role_code
|
|
154
|
+
) THEN
|
|
155
|
+
RAISE EXCEPTION 'Invalid role code: %', p_new_role_code
|
|
156
|
+
USING ERRCODE = '22023';
|
|
157
|
+
END IF;
|
|
158
|
+
|
|
159
|
+
SELECT EXISTS (
|
|
160
|
+
SELECT 1 FROM public.profiles p WHERE p.id = p_target_profile_id
|
|
161
|
+
) INTO v_target_exists;
|
|
162
|
+
|
|
163
|
+
IF NOT v_target_exists THEN
|
|
164
|
+
RETURN QUERY SELECT false, 'not_found', NULL::text, p_new_role_code;
|
|
165
|
+
RETURN;
|
|
166
|
+
END IF;
|
|
167
|
+
|
|
168
|
+
SELECT ura.role_code
|
|
169
|
+
INTO v_old_role
|
|
170
|
+
FROM public.user_role_assignments ura
|
|
171
|
+
WHERE ura.profile_id = p_target_profile_id
|
|
172
|
+
LIMIT 1;
|
|
173
|
+
|
|
174
|
+
IF v_old_role = p_new_role_code THEN
|
|
175
|
+
RETURN QUERY SELECT false, 'already_role', v_old_role, p_new_role_code;
|
|
176
|
+
RETURN;
|
|
177
|
+
END IF;
|
|
178
|
+
|
|
179
|
+
IF v_old_role = 'admin' AND p_new_role_code <> 'admin' THEN
|
|
180
|
+
SELECT count(*)
|
|
181
|
+
INTO v_admin_count
|
|
182
|
+
FROM public.user_role_assignments ura
|
|
183
|
+
WHERE ura.role_code = 'admin';
|
|
184
|
+
|
|
185
|
+
IF v_admin_count <= 1 THEN
|
|
186
|
+
RETURN QUERY SELECT false, 'last_admin_guard', v_old_role, p_new_role_code;
|
|
187
|
+
RETURN;
|
|
188
|
+
END IF;
|
|
189
|
+
END IF;
|
|
190
|
+
|
|
191
|
+
INSERT INTO public.user_role_assignments (
|
|
192
|
+
profile_id,
|
|
193
|
+
role_code,
|
|
194
|
+
assigned_by_profile_id,
|
|
195
|
+
assigned_at,
|
|
196
|
+
reason
|
|
197
|
+
)
|
|
198
|
+
VALUES (
|
|
199
|
+
p_target_profile_id,
|
|
200
|
+
p_new_role_code,
|
|
201
|
+
v_actor_profile_id,
|
|
202
|
+
now(),
|
|
203
|
+
NULLIF(trim(COALESCE(p_reason, '')), '')
|
|
204
|
+
)
|
|
205
|
+
ON CONFLICT (profile_id)
|
|
206
|
+
DO UPDATE
|
|
207
|
+
SET role_code = EXCLUDED.role_code,
|
|
208
|
+
assigned_by_profile_id = EXCLUDED.assigned_by_profile_id,
|
|
209
|
+
assigned_at = EXCLUDED.assigned_at,
|
|
210
|
+
reason = EXCLUDED.reason;
|
|
211
|
+
|
|
212
|
+
INSERT INTO public.role_change_audit (
|
|
213
|
+
target_profile_id,
|
|
214
|
+
changed_by_profile_id,
|
|
215
|
+
old_role_code,
|
|
216
|
+
new_role_code,
|
|
217
|
+
reason,
|
|
218
|
+
created_at
|
|
219
|
+
) VALUES (
|
|
220
|
+
p_target_profile_id,
|
|
221
|
+
v_actor_profile_id,
|
|
222
|
+
v_old_role,
|
|
223
|
+
p_new_role_code,
|
|
224
|
+
NULLIF(trim(COALESCE(p_reason, '')), ''),
|
|
225
|
+
now()
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
RETURN QUERY SELECT true, 'changed', v_old_role, p_new_role_code;
|
|
229
|
+
END;
|
|
230
|
+
$$;
|
|
231
|
+
|
|
232
|
+
CREATE OR REPLACE FUNCTION public.guard_privileged_profile_fields()
|
|
233
|
+
RETURNS trigger
|
|
234
|
+
LANGUAGE plpgsql
|
|
235
|
+
SECURITY DEFINER
|
|
236
|
+
SET search_path = public
|
|
237
|
+
AS $$
|
|
238
|
+
BEGIN
|
|
239
|
+
IF auth.role() = 'service_role' THEN
|
|
240
|
+
RETURN NEW;
|
|
241
|
+
END IF;
|
|
242
|
+
|
|
243
|
+
IF public.is_admin() THEN
|
|
244
|
+
RETURN NEW;
|
|
245
|
+
END IF;
|
|
246
|
+
|
|
247
|
+
IF public.is_staff() THEN
|
|
248
|
+
IF NEW.user_id IS DISTINCT FROM OLD.user_id
|
|
249
|
+
OR NEW.email IS DISTINCT FROM OLD.email
|
|
250
|
+
OR NEW.created_at IS DISTINCT FROM OLD.created_at
|
|
251
|
+
THEN
|
|
252
|
+
RAISE EXCEPTION 'Staff cannot modify privileged profile fields'
|
|
253
|
+
USING ERRCODE = '42501';
|
|
254
|
+
END IF;
|
|
255
|
+
END IF;
|
|
256
|
+
|
|
257
|
+
RETURN NEW;
|
|
258
|
+
END;
|
|
259
|
+
$$;
|
|
260
|
+
|
|
261
|
+
DROP TRIGGER IF EXISTS guard_privileged_profile_fields ON public.profiles;
|
|
262
|
+
CREATE TRIGGER guard_privileged_profile_fields
|
|
263
|
+
BEFORE UPDATE ON public.profiles
|
|
264
|
+
FOR EACH ROW
|
|
265
|
+
EXECUTE FUNCTION public.guard_privileged_profile_fields();
|
|
266
|
+
|
|
267
|
+
CREATE OR REPLACE FUNCTION public.ensure_profile_role_assignment()
|
|
268
|
+
RETURNS trigger
|
|
269
|
+
LANGUAGE plpgsql
|
|
270
|
+
SECURITY DEFINER
|
|
271
|
+
SET search_path = public
|
|
272
|
+
AS $$
|
|
273
|
+
BEGIN
|
|
274
|
+
INSERT INTO public.user_role_assignments (
|
|
275
|
+
profile_id,
|
|
276
|
+
role_code,
|
|
277
|
+
assigned_at,
|
|
278
|
+
assigned_by_profile_id,
|
|
279
|
+
reason
|
|
280
|
+
)
|
|
281
|
+
VALUES (
|
|
282
|
+
NEW.id,
|
|
283
|
+
'client',
|
|
284
|
+
now(),
|
|
285
|
+
NULL,
|
|
286
|
+
'auto_profile_default_client_role'
|
|
287
|
+
)
|
|
288
|
+
ON CONFLICT (profile_id)
|
|
289
|
+
DO NOTHING;
|
|
290
|
+
|
|
291
|
+
RETURN NEW;
|
|
292
|
+
END;
|
|
293
|
+
$$;
|
|
294
|
+
|
|
295
|
+
DROP TRIGGER IF EXISTS trg_ensure_profile_role_assignment ON public.profiles;
|
|
296
|
+
CREATE TRIGGER trg_ensure_profile_role_assignment
|
|
297
|
+
AFTER INSERT ON public.profiles
|
|
298
|
+
FOR EACH ROW
|
|
299
|
+
EXECUTE FUNCTION public.ensure_profile_role_assignment();
|
|
300
|
+
|
|
301
|
+
INSERT INTO public.user_role_assignments (
|
|
302
|
+
profile_id,
|
|
303
|
+
role_code,
|
|
304
|
+
assigned_at,
|
|
305
|
+
assigned_by_profile_id,
|
|
306
|
+
reason
|
|
307
|
+
)
|
|
308
|
+
SELECT
|
|
309
|
+
p.id,
|
|
310
|
+
'client',
|
|
311
|
+
now(),
|
|
312
|
+
NULL,
|
|
313
|
+
'auto_backfill_missing_profile_role'
|
|
314
|
+
FROM public.profiles p
|
|
315
|
+
LEFT JOIN public.user_role_assignments ura
|
|
316
|
+
ON ura.profile_id = p.id
|
|
317
|
+
WHERE ura.profile_id IS NULL;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Core Migrations
|
|
2
|
+
|
|
3
|
+
`core` owns shared platform foundations that every client instance needs.
|
|
4
|
+
|
|
5
|
+
## Owns
|
|
6
|
+
|
|
7
|
+
- `profiles`
|
|
8
|
+
- auth/profile sync triggers
|
|
9
|
+
- shared helper functions like `current_profile_id()`
|
|
10
|
+
- shared app events / alerts foundations
|
|
11
|
+
- rate limiting primitives
|
|
12
|
+
- global notification/language preferences
|
|
13
|
+
- any always-on shared platform table or helper
|
|
14
|
+
|
|
15
|
+
## Does not own
|
|
16
|
+
|
|
17
|
+
- module-specific CRM tables
|
|
18
|
+
- project collaboration tables
|
|
19
|
+
- admin-only role governance tables
|
|
20
|
+
- client-only tables
|
|
21
|
+
|
|
22
|
+
## Apply order
|
|
23
|
+
|
|
24
|
+
Apply `core` before any module migrations.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|