spine-framework 0.3.132 → 0.3.134

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.
@@ -103,14 +103,17 @@ export async function runMigrations(opts: { frameworkOnly?: boolean } = {}): Pro
103
103
  const statements = splitSqlStatements(sql)
104
104
 
105
105
  try {
106
+ await client.query('BEGIN')
106
107
  for (const stmt of statements) await client.query(stmt)
107
108
  await client.query(
108
109
  `INSERT INTO _spine_migrations (filename) VALUES ($1) ON CONFLICT (filename) DO NOTHING`,
109
110
  [name]
110
111
  )
112
+ await client.query('COMMIT')
111
113
  console.log('✓')
112
114
  applied++
113
115
  } catch (err: any) {
116
+ await client.query('ROLLBACK').catch(() => {})
114
117
  console.log('✗')
115
118
  console.error(` ${err.message}`)
116
119
  failed++
@@ -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"]
@@ -319,7 +319,7 @@ CREATE TABLE IF NOT EXISTS public.account_paths (
319
319
  ancestor_id uuid NOT NULL REFERENCES public.accounts(id) ON DELETE CASCADE,
320
320
  descendant_id uuid NOT NULL REFERENCES public.accounts(id) ON DELETE CASCADE,
321
321
  depth integer NOT NULL,
322
- PRIMARY KEY (ancestor_id, descendant_id)
322
+ PRIMARY KEY (ancestor_id, descendant_id),
323
323
  CONSTRAINT chk_account_paths_no_self CHECK (ancestor_id != descendant_id)
324
324
  );
325
325
  CREATE INDEX IF NOT EXISTS idx_accounts_parent_id ON public.accounts(parent_id);
@@ -530,10 +530,6 @@ CREATE TRIGGER set_updated_at_account_paths
530
530
  BEFORE UPDATE ON public.account_paths
531
531
  FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
532
532
 
533
- DROP TRIGGER IF EXISTS set_updated_at_people_roles ON public.people_roles;
534
- CREATE TRIGGER set_updated_at_people_roles
535
- BEFORE UPDATE ON public.people_roles
536
- FOR EACH ROW EXECUTE FUNCTION public.set_updated_at();
537
533
 
538
534
  -- RLS policies
539
535
 
@@ -634,10 +630,6 @@ CREATE POLICY api_keys_access ON public.api_keys FOR ALL
634
630
  USING (account_id IN (SELECT public.get_accessible_accounts(auth.uid()))
635
631
  OR id = auth.uid() );
636
632
 
637
- CREATE POLICY akul_access ON public.api_key_usage_logs FOR ALL
638
- USING (account_id IN (SELECT public.get_accessible_accounts(auth.uid()))
639
- );
640
-
641
633
  CREATE POLICY logs_access ON public.logs FOR ALL
642
634
  USING (account_id IN (SELECT public.get_accessible_accounts(auth.uid()))
643
635
  );
@@ -122,6 +122,7 @@ COMMENT ON COLUMN public.integrations.is_configured IS 'True when required crede
122
122
  ALTER TABLE public.integrations DROP CONSTRAINT IF EXISTS integrations_app_id_slug_key;
123
123
  ALTER TABLE public.integrations ADD CONSTRAINT integrations_app_account_slug_key UNIQUE (app_id, account_id, slug);
124
124
 
125
+ ALTER TABLE public.integrations DROP CONSTRAINT IF EXISTS fk_integrations_created_by;
125
126
  ALTER TABLE public.integrations ADD CONSTRAINT fk_integrations_created_by
126
127
  FOREIGN KEY (created_by) REFERENCES public.people(id) ON DELETE SET NULL;
127
128
 
@@ -153,6 +154,7 @@ COMMENT ON COLUMN public.api_keys.rate_limit IS 'Requests per rate_limit_window_
153
154
 
154
155
  ALTER TABLE public.api_keys ADD CONSTRAINT fk_api_keys_integration
155
156
  FOREIGN KEY (integration_id) REFERENCES public.integrations(id) ON DELETE CASCADE;
157
+ ALTER TABLE public.api_keys DROP CONSTRAINT IF EXISTS fk_api_keys_created_by;
156
158
  ALTER TABLE public.api_keys ADD CONSTRAINT fk_api_keys_created_by
157
159
  FOREIGN KEY (created_by) REFERENCES public.people(id) ON DELETE SET NULL;
158
160
 
@@ -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();
@@ -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.132",
3
+ "version": "0.3.134",
4
4
  "description": "Multi-tenant, modular application platform for modern SaaS systems",
5
5
  "type": "module",
6
6
  "bin": {
@@ -25,6 +25,7 @@
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",
28
29
  ".framework/public/",
29
30
  ".framework/scripts/",
30
31
  ".framework/seeds/",