spine-framework 0.3.61 → 0.3.63

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.
@@ -301,6 +301,8 @@ interface RegistrationConfig {
301
301
  enabled: boolean
302
302
  default_role: string
303
303
  redirect_path: string
304
+ account_strategy?: 'existing' | 'new' | 'choice'
305
+ target_account?: string
304
306
  }
305
307
 
306
308
  interface SpineConfig {
@@ -313,6 +315,8 @@ interface SpineConfig {
313
315
  default_role: string
314
316
  path: string
315
317
  enabled: boolean
318
+ account_strategy: 'existing' | 'new' | 'choice'
319
+ target_account?: string
316
320
  }>
317
321
  }
318
322
  }
@@ -360,12 +364,14 @@ function updateRegistrationConfig(appSlug: string, regConfig: RegistrationConfig
360
364
  console.log(` ⚠️ Could not load roles from seed data, using default role only`)
361
365
  }
362
366
 
363
- // Add/update this app's registration entry with multi-role support
367
+ // Add/update this app's registration entry with multi-role and account strategy support
364
368
  config.registration.apps[appSlug] = {
365
369
  roles: availableRoles,
366
370
  default_role: regConfig.default_role,
367
371
  path: regConfig.redirect_path,
368
372
  enabled: regConfig.enabled,
373
+ account_strategy: regConfig.account_strategy || 'new',
374
+ target_account: regConfig.target_account,
369
375
  }
370
376
 
371
377
  // If this is the first registration-enabled app, make it the default
@@ -213,12 +213,79 @@ export const validateToken = createHandler(async (_ctx, _body) => {
213
213
  * (invite path is handled entirely in the DB trigger)
214
214
  */
215
215
  export const completeRegistration = createHandler(async (ctx, body) => {
216
- const { path, account_name, email, role_slug } = body
216
+ const { path, account_name, email, role_slug, target_account } = body
217
217
 
218
218
  if (!path || !email) {
219
219
  throw new Error('path and email are required')
220
220
  }
221
221
 
222
+ if (path === 'existing_account') {
223
+ if (!target_account) throw new Error('target_account is required for existing_account path')
224
+
225
+ // Resolve the target account
226
+ const { data: targetAccount } = await adminDb
227
+ .from('accounts')
228
+ .select('id, slug')
229
+ .eq('slug', target_account)
230
+ .single()
231
+
232
+ if (!targetAccount) {
233
+ throw new Error(`Target account '${target_account}' not found`)
234
+ }
235
+
236
+ // Resolve person type_id
237
+ const { data: personType } = await adminDb
238
+ .from('types')
239
+ .select('id, design_schema, validation_schema')
240
+ .eq('kind', 'person')
241
+ .eq('slug', 'person')
242
+ .single()
243
+
244
+ // Resolve role from role_slug param
245
+ const targetRoleSlug = role_slug || 'member'
246
+
247
+ let { data: roles } = await adminDb
248
+ .from('roles')
249
+ .select('id, slug')
250
+ .eq('slug', targetRoleSlug)
251
+ .limit(1)
252
+
253
+ let role = roles?.[0]
254
+
255
+ // If specific role not found, try member_admin or member as fallback
256
+ if (!role) {
257
+ const { data: fallbackRoles } = await adminDb
258
+ .from('roles')
259
+ .select('id, slug')
260
+ .in('slug', ['member_admin', 'member'])
261
+ .order('slug')
262
+ .limit(1)
263
+ role = fallbackRoles?.[0]
264
+ }
265
+
266
+ // Update the walk-up shell to be an active user on the target account
267
+ const { error: updateErr } = await adminDb
268
+ .from('people')
269
+ .update({
270
+ account_id: targetAccount.id,
271
+ role_id: role?.id ?? null,
272
+ status: 'active',
273
+ type_id: personType.id,
274
+ data: {},
275
+ updated_at: new Date().toISOString(),
276
+ })
277
+ .eq('email', email)
278
+ .eq('status', 'active')
279
+ .is('role_id', null)
280
+
281
+ if (updateErr) throw new Error('Failed to assign user to existing account: ' + updateErr.message)
282
+
283
+ return {
284
+ status: 'active',
285
+ account_slug: targetAccount.slug
286
+ }
287
+ }
288
+
222
289
  if (path === 'new_account') {
223
290
  if (!account_name) throw new Error('account_name is required for new_account path')
224
291
 
@@ -57,6 +57,8 @@ interface RegistrationConfig {
57
57
  default_role: string
58
58
  path: string
59
59
  enabled: boolean
60
+ account_strategy: 'existing' | 'new' | 'choice'
61
+ target_account?: string
60
62
  }>
61
63
  }
62
64
 
@@ -153,6 +155,9 @@ export function RegisterPage() {
153
155
  const [domainMatchChoice, setDomainMatchChoice] = useState<DomainMatchChoice>(null)
154
156
  const [domainCheckLoading, setDomainCheckLoading] = useState(false)
155
157
 
158
+ // Account strategy state
159
+ const [accountStrategy, setAccountStrategy] = useState<'existing' | 'new'>('new')
160
+
156
161
  const [error, setError] = useState('')
157
162
  const [isLoading, setIsLoading] = useState(false)
158
163
 
@@ -235,26 +240,44 @@ export function RegisterPage() {
235
240
  // 4. Walk-up paths — call complete-registration
236
241
  const resolvedEmail = email
237
242
 
238
- // Determine role from URL params or config
243
+ // Determine role and account strategy from URL params or config
239
244
  const urlRole = searchParams.get('role')
240
245
  const urlApp = searchParams.get('app')
241
246
 
242
247
  let effectiveRole = regConfig?.default_role || 'member'
248
+ let effectiveAccountStrategy: 'existing' | 'new' | 'choice' = 'new'
249
+ let targetAccount: string | undefined
243
250
 
244
- // If app is specified, use app's default role
251
+ // If app is specified, use app's configuration
245
252
  if (urlApp && regConfig?.apps[urlApp]) {
246
253
  const appConfig = regConfig.apps[urlApp]
247
254
 
248
- // If URL role is provided, validate it against app's available roles
255
+ console.log('App config found:', appConfig)
256
+
257
+ // Determine role
249
258
  if (urlRole && appConfig.roles.includes(urlRole)) {
250
259
  effectiveRole = urlRole
251
260
  } else {
252
261
  effectiveRole = appConfig.default_role
253
262
  }
263
+
264
+ // Determine account strategy
265
+ effectiveAccountStrategy = appConfig.account_strategy
266
+ targetAccount = appConfig.target_account
267
+
268
+ console.log('Account strategy resolved:', { effectiveAccountStrategy, targetAccount })
254
269
  }
255
- // If only URL role is specified (no app), use it directly
270
+ // If only URL role is specified (no app), use it directly with default strategy
256
271
  else if (urlRole) {
257
272
  effectiveRole = urlRole
273
+ effectiveAccountStrategy = 'new'
274
+ }
275
+
276
+ // For 'existing' strategy, always use the target account
277
+ if (effectiveAccountStrategy === 'existing') {
278
+ setAccountStrategy('existing')
279
+ } else {
280
+ setAccountStrategy('new')
258
281
  }
259
282
 
260
283
  if (domainMatchChoice === 'join') {
@@ -273,17 +296,33 @@ export function RegisterPage() {
273
296
  return
274
297
  }
275
298
 
276
- // New account (personal or company create_new)
277
- const accountName = useType === 'personal' ? fullName : companyName
278
- const newResult = await callInvitesApi('complete-registration', {
279
- path: 'new_account',
280
- email: resolvedEmail,
281
- account_name: accountName,
282
- role_slug: effectiveRole,
283
- }, 'POST')
284
-
285
- if (newResult.error) {
286
- setError(newResult.error)
299
+ // Handle account creation vs existing account assignment
300
+ let registrationResult
301
+ console.log('Registration decision:', { accountStrategy, targetAccount, effectiveRole })
302
+
303
+ if (accountStrategy === 'existing' && targetAccount) {
304
+ // Assign to existing account
305
+ console.log('Using existing_account path')
306
+ registrationResult = await callInvitesApi('complete-registration', {
307
+ path: 'existing_account',
308
+ email: resolvedEmail,
309
+ target_account: targetAccount,
310
+ role_slug: effectiveRole,
311
+ }, 'POST')
312
+ } else {
313
+ // Create new account (personal or company)
314
+ console.log('Using new_account path')
315
+ const accountName = useType === 'personal' ? fullName : companyName
316
+ registrationResult = await callInvitesApi('complete-registration', {
317
+ path: 'new_account',
318
+ email: resolvedEmail,
319
+ account_name: accountName,
320
+ role_slug: effectiveRole,
321
+ }, 'POST')
322
+ }
323
+
324
+ if (registrationResult.error) {
325
+ setError(registrationResult.error)
287
326
  return
288
327
  }
289
328
 
@@ -325,7 +364,7 @@ export function RegisterPage() {
325
364
  const isInvite = !!inviteToken && !!invitePreview
326
365
  const showDomainMatchPrompt = domainMatchAccountId && domainMatchChoice === null && useType === 'company' && !isFirstUser
327
366
  const effectiveEmail = isInvite ? invitePreview.email : email
328
- const showUseTypeToggle = !isInvite && !isFirstUser
367
+ const showUseTypeToggle = !isInvite && !isFirstUser && accountStrategy === 'new'
329
368
 
330
369
  // Header text based on scenario
331
370
  let headerTitle = 'Create your account'
@@ -478,8 +517,8 @@ export function RegisterPage() {
478
517
  </div>
479
518
  )}
480
519
 
481
- {/* Company name — shown for company type when not joining existing */}
482
- {!isInvite && useType === 'company' && domainMatchChoice !== 'join' && !isFirstUser && (
520
+ {/* Company name — shown for company type when not joining existing and using new account strategy */}
521
+ {!isInvite && useType === 'company' && domainMatchChoice !== 'join' && !isFirstUser && accountStrategy === 'new' && (
483
522
  <div>
484
523
  <label htmlFor="companyName" className="block text-sm font-medium text-slate-700">
485
524
  Company name
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spine-framework",
3
- "version": "0.3.61",
3
+ "version": "0.3.63",
4
4
  "description": "Multi-tenant, modular application platform for modern SaaS systems",
5
5
  "type": "module",
6
6
  "bin": {
package/spine.config.json CHANGED
@@ -9,13 +9,16 @@
9
9
  "roles": ["member"],
10
10
  "default_role": "member",
11
11
  "path": "/portal",
12
- "enabled": true
12
+ "enabled": true,
13
+ "account_strategy": "new"
13
14
  },
14
15
  "cortex": {
15
16
  "roles": ["support", "support_admin"],
16
17
  "default_role": "support",
17
18
  "path": "/cortex",
18
- "enabled": true
19
+ "enabled": true,
20
+ "account_strategy": "existing",
21
+ "target_account": "spine-system"
19
22
  }
20
23
  }
21
24
  }