spine-framework 0.3.133 → 0.3.135

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.
@@ -137,7 +137,7 @@ function isAdminRole(roleSlug: string | undefined): boolean {
137
137
  // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
138
138
  // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
139
139
  // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
140
- export const checkFirstUser = createHandler(async (_ctx, _body) => {
140
+ export const checkFirstUser = createPublicHandler(async (_ctx, _body) => {
141
141
  const { count, error } = await adminDb
142
142
  .from('people')
143
143
  .select('*', { count: 'exact', head: true })
@@ -161,7 +161,7 @@ export const checkFirstUser = createHandler(async (_ctx, _body) => {
161
161
  // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
162
162
  // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
163
163
  // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
164
- export const checkDomain = createHandler(async (_ctx, _body) => {
164
+ export const checkDomain = createPublicHandler(async (_ctx, _body) => {
165
165
  const email = _ctx.query?.email as string | undefined
166
166
  if (!email || !email.includes('@')) {
167
167
  throw new Error('Valid email is required')
@@ -175,12 +175,13 @@ export const checkDomain = createHandler(async (_ctx, _body) => {
175
175
 
176
176
  const { data } = await adminDb
177
177
  .from('people')
178
- .select('id')
178
+ .select('account_id')
179
179
  .ilike('email', `%@${domain}`)
180
180
  .eq('status', 'active')
181
181
  .limit(1)
182
182
 
183
- return { match: (data?.length ?? 0) > 0 }
183
+ const first = data?.[0]
184
+ return { match: !!first, account_id: first?.account_id ?? null }
184
185
  })
185
186
  // CIL[END chunk_id="invites_checkdomain_1783968140"]
186
187
  // CIL[BEGIN chunk_id="invites_validatetoken_1783968140" type="function" identity="validateToken" parent="none"]
@@ -197,7 +198,7 @@ export const checkDomain = createHandler(async (_ctx, _body) => {
197
198
  // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
198
199
  // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
199
200
  // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
200
- export const validateToken = createHandler(async (_ctx, _body) => {
201
+ export const validateToken = createPublicHandler(async (_ctx, _body) => {
201
202
  const token = _ctx.query?.token as string | undefined
202
203
  if (!token) throw new Error('Token is required')
203
204
 
@@ -0,0 +1,82 @@
1
+ -- Create trigger function to sync new auth users to people records
2
+ CREATE OR REPLACE FUNCTION public.handle_new_auth_user()
3
+ RETURNS TRIGGER LANGUAGE plpgsql SECURITY DEFINER SET search_path = public AS $$
4
+ DECLARE
5
+ v_account_id uuid;
6
+ v_type_id uuid;
7
+ v_role_id uuid;
8
+ v_is_first boolean;
9
+ v_design_schema jsonb;
10
+ v_validation_schema jsonb;
11
+ v_stub_id uuid;
12
+ v_stub_status text;
13
+ BEGIN
14
+ SELECT id INTO v_account_id FROM public.accounts WHERE slug = 'spine-system' LIMIT 1;
15
+ SELECT id, design_schema, validation_schema
16
+ INTO v_type_id, v_design_schema, v_validation_schema
17
+ FROM public.types WHERE kind = 'person' AND slug = 'person' LIMIT 1;
18
+
19
+ IF v_account_id IS NULL OR v_type_id IS NULL THEN
20
+ RAISE WARNING 'handle_new_auth_user: spine-system account or person type not found — skipping';
21
+ RETURN NEW;
22
+ END IF;
23
+
24
+ SELECT id, status
25
+ INTO v_stub_id, v_stub_status
26
+ FROM public.people
27
+ WHERE email = NEW.email
28
+ AND status IN ('invited', 'pending')
29
+ AND auth_uid IS NULL
30
+ LIMIT 1;
31
+
32
+ IF v_stub_id IS NOT NULL THEN
33
+ UPDATE public.people SET
34
+ auth_uid = NEW.id,
35
+ full_name = COALESCE(NEW.raw_user_meta_data->>'full_name', full_name, split_part(NEW.email, '@', 1)),
36
+ status = CASE WHEN v_stub_status = 'invited' THEN 'active' ELSE 'pending' END,
37
+ updated_at = now()
38
+ WHERE id = v_stub_id;
39
+
40
+ RETURN NEW;
41
+ END IF;
42
+
43
+ SELECT NOT EXISTS (SELECT 1 FROM public.people LIMIT 1) INTO v_is_first;
44
+ IF v_is_first THEN
45
+ SELECT id INTO v_role_id FROM public.roles WHERE slug = 'system_admin' LIMIT 1;
46
+ END IF;
47
+
48
+ INSERT INTO public.people (
49
+ id,
50
+ auth_uid,
51
+ email,
52
+ full_name,
53
+ account_id,
54
+ type_id,
55
+ role_id,
56
+ status,
57
+ is_active,
58
+ design_schema,
59
+ validation_schema
60
+ ) VALUES (
61
+ NEW.id,
62
+ NEW.id,
63
+ NEW.email,
64
+ COALESCE(NEW.raw_user_meta_data->>'full_name', split_part(NEW.email, '@', 1)),
65
+ v_account_id,
66
+ v_type_id,
67
+ v_role_id,
68
+ 'active',
69
+ true,
70
+ v_design_schema,
71
+ COALESCE(v_validation_schema, '{}'::jsonb)
72
+ )
73
+ ON CONFLICT (id) DO NOTHING;
74
+
75
+ RETURN NEW;
76
+ END;
77
+ $$;
78
+
79
+ DROP TRIGGER IF EXISTS on_auth_user_created ON auth.users;
80
+ CREATE TRIGGER on_auth_user_created
81
+ AFTER INSERT ON auth.users
82
+ FOR EACH ROW EXECUTE FUNCTION public.handle_new_auth_user();
@@ -0,0 +1,15 @@
1
+ -- Seed the Spine system master account required by the auth trigger and existing-account registration flow
2
+ INSERT INTO public.accounts (id, slug, display_name, description, data, is_active, design_schema, validation_schema, type_id)
3
+ SELECT
4
+ gen_random_uuid(),
5
+ 'spine-system',
6
+ 'Spine System',
7
+ 'Master system account for internal Spine operations',
8
+ '{}'::jsonb,
9
+ true,
10
+ '{}'::jsonb,
11
+ '{}'::jsonb,
12
+ id
13
+ FROM public.types
14
+ WHERE kind = 'account' AND slug = 'account'
15
+ ON CONFLICT (slug) DO NOTHING;
@@ -262,8 +262,8 @@ export function RegisterPage() {
262
262
  const result = await callInvitesApi('check-domain', { email })
263
263
  setDomainCheckLoading(false)
264
264
 
265
- if (result.data?.match) {
266
- setDomainMatchAccountId('__matched__')
265
+ if (result.data?.match && result.data?.account_id) {
266
+ setDomainMatchAccountId(result.data.account_id)
267
267
  }
268
268
  }, [email, inviteToken, useType, isFirstUser])
269
269
 
@@ -323,23 +323,22 @@ export function RegisterPage() {
323
323
 
324
324
  const completeRegistrationParams: any = {
325
325
  email: resolvedEmail,
326
- full_name: fullName,
327
- app: effectiveApp,
328
- role: effectiveRole,
329
- redirect: getPostRegistrationRedirect(regConfig, searchParams, false, effectiveRole),
326
+ path: 'new_account',
327
+ role_slug: effectiveRole,
330
328
  }
331
329
 
332
330
  if (useType === 'company') {
333
- completeRegistrationParams.account_name = companyName
331
+ completeRegistrationParams.account_name = companyName || fullName || email.split('@')[0]
332
+ } else {
333
+ completeRegistrationParams.account_name = fullName || email.split('@')[0]
334
334
  }
335
335
 
336
336
  if (effectiveAccountStrategy === 'existing' && targetAccount) {
337
- completeRegistrationParams.account_id = targetAccount
338
- completeRegistrationParams.strategy = 'join_request'
339
- } else if (domainMatchAccountId === '__matched__' && domainMatchChoice === 'join') {
340
- completeRegistrationParams.strategy = 'join_request'
341
- } else {
342
- completeRegistrationParams.strategy = 'new_account'
337
+ completeRegistrationParams.path = 'existing_account'
338
+ completeRegistrationParams.target_account = targetAccount
339
+ } else if (domainMatchAccountId && domainMatchChoice === 'join') {
340
+ completeRegistrationParams.path = 'join_request'
341
+ completeRegistrationParams.target_account_id = domainMatchAccountId
343
342
  }
344
343
 
345
344
  const result = await callInvitesApi('complete-registration', completeRegistrationParams, 'POST')
@@ -367,7 +366,7 @@ export function RegisterPage() {
367
366
  const isInvite = !!inviteToken && !tokenError
368
367
  const showPersonalCompanyToggle = !isInvite && !isFirstUser
369
368
  const showCompanyName = showPersonalCompanyToggle && useType === 'company'
370
- const showDomainMatchChoice = domainMatchAccountId === '__matched__' && !isInvite && !isFirstUser
369
+ const showDomainMatchChoice = !!domainMatchAccountId && !isInvite && !isFirstUser
371
370
 
372
371
  if (tokenLoading || isFirstUser === null || (!isFirstUser && !isInvite && !regConfig)) {
373
372
  return (
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spine-framework",
3
- "version": "0.3.133",
3
+ "version": "0.3.135",
4
4
  "description": "Multi-tenant, modular application platform for modern SaaS systems",
5
5
  "type": "module",
6
6
  "bin": {
@@ -25,6 +25,8 @@
25
25
  ".framework/migrations/007_kb_article_list.sql",
26
26
  ".framework/migrations/008_items_target.sql",
27
27
  ".framework/migrations/009_roles_slug_unique.sql",
28
+ ".framework/migrations/010_auth_user_trigger.sql",
29
+ ".framework/migrations/011_spine_system_account.sql",
28
30
  ".framework/public/",
29
31
  ".framework/scripts/",
30
32
  ".framework/seeds/",