spine-framework 0.3.60 → 0.3.62

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
  }
@@ -345,12 +349,29 @@ function updateRegistrationConfig(appSlug: string, regConfig: RegistrationConfig
345
349
  config.registration.apps = {}
346
350
  }
347
351
 
348
- // Add/update this app's registration entry with multi-role support
352
+ // Try to load all roles from the app's seed data
353
+ let availableRoles: string[] = [regConfig.default_role] // Start with default role from manifest
354
+
355
+ try {
356
+ const rolesPath = join(PROJECT_ROOT, 'custom/apps', appSlug, 'seed/roles.json')
357
+ if (existsSync(rolesPath)) {
358
+ const rolesData = JSON.parse(readFileSync(rolesPath, 'utf8'))
359
+ if (Array.isArray(rolesData)) {
360
+ availableRoles = rolesData.map((role: any) => role.slug).filter(Boolean)
361
+ }
362
+ }
363
+ } catch (error) {
364
+ console.log(` ⚠️ Could not load roles from seed data, using default role only`)
365
+ }
366
+
367
+ // Add/update this app's registration entry with multi-role and account strategy support
349
368
  config.registration.apps[appSlug] = {
350
- roles: [regConfig.default_role], // Start with default role from manifest
369
+ roles: availableRoles,
351
370
  default_role: regConfig.default_role,
352
371
  path: regConfig.redirect_path,
353
372
  enabled: regConfig.enabled,
373
+ account_strategy: regConfig.account_strategy || 'new',
374
+ target_account: regConfig.target_account,
354
375
  }
355
376
 
356
377
  // If this is the first registration-enabled app, make it the default
@@ -367,4 +388,5 @@ function updateRegistrationConfig(appSlug: string, regConfig: RegistrationConfig
367
388
 
368
389
  saveSpineConfig(config)
369
390
  console.log(` ✓ Updated spine.config.json registration for ${appSlug}`)
391
+ console.log(` ✓ Available roles: ${availableRoles.join(', ')}`)
370
392
  }
@@ -213,13 +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
217
- console.log('completeRegistration called with:', { path, account_name, email, role_slug })
216
+ const { path, account_name, email, role_slug, target_account } = body
218
217
 
219
218
  if (!path || !email) {
220
219
  throw new Error('path and email are required')
221
220
  }
222
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
+
223
289
  if (path === 'new_account') {
224
290
  if (!account_name) throw new Error('account_name is required for new_account path')
225
291
 
@@ -241,7 +307,6 @@ export const completeRegistration = createHandler(async (ctx, body) => {
241
307
 
242
308
  // Resolve role from role_slug param, or fall back to member_admin/member lookup
243
309
  const targetRoleSlug = role_slug || 'member'
244
- console.log('Looking for role:', targetRoleSlug)
245
310
 
246
311
  let { data: roles } = await adminDb
247
312
  .from('roles')
@@ -250,11 +315,9 @@ export const completeRegistration = createHandler(async (ctx, body) => {
250
315
  .limit(1)
251
316
 
252
317
  let role = roles?.[0]
253
- console.log('Found role:', role)
254
318
 
255
319
  // If specific role not found, try member_admin or member as fallback
256
320
  if (!role) {
257
- console.log('Role not found, trying fallbacks...')
258
321
  const { data: fallbackRoles } = await adminDb
259
322
  .from('roles')
260
323
  .select('id, slug')
@@ -262,7 +325,6 @@ export const completeRegistration = createHandler(async (ctx, body) => {
262
325
  .order('slug')
263
326
  .limit(1)
264
327
  role = fallbackRoles?.[0]
265
- console.log('Fallback role:', role)
266
328
  }
267
329
 
268
330
  // If no roles exist yet and no specific role was requested, create a default member role
@@ -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,30 +240,41 @@ 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
- console.log('Role resolution:', { urlRole, urlApp, regConfig })
242
246
 
243
247
  let effectiveRole = regConfig?.default_role || 'member'
248
+ let effectiveAccountStrategy: 'existing' | 'new' | 'choice' = 'new'
249
+ let targetAccount: string | undefined
244
250
 
245
- // If app is specified, use app's default role
251
+ // If app is specified, use app's configuration
246
252
  if (urlApp && regConfig?.apps[urlApp]) {
247
253
  const appConfig = regConfig.apps[urlApp]
248
254
 
249
- // If URL role is provided, validate it against app's available roles
255
+ // Determine role
250
256
  if (urlRole && appConfig.roles.includes(urlRole)) {
251
257
  effectiveRole = urlRole
252
258
  } else {
253
259
  effectiveRole = appConfig.default_role
254
260
  }
261
+
262
+ // Determine account strategy
263
+ effectiveAccountStrategy = appConfig.account_strategy
264
+ targetAccount = appConfig.target_account
255
265
  }
256
- // If only URL role is specified (no app), use it directly
266
+ // If only URL role is specified (no app), use it directly with default strategy
257
267
  else if (urlRole) {
258
268
  effectiveRole = urlRole
269
+ effectiveAccountStrategy = 'new'
259
270
  }
260
271
 
261
- console.log('Effective role:', effectiveRole)
272
+ // For 'existing' strategy, always use the target account
273
+ if (effectiveAccountStrategy === 'existing') {
274
+ setAccountStrategy('existing')
275
+ } else {
276
+ setAccountStrategy('new')
277
+ }
262
278
 
263
279
  if (domainMatchChoice === 'join') {
264
280
  // Join existing account as access_request
@@ -276,17 +292,29 @@ export function RegisterPage() {
276
292
  return
277
293
  }
278
294
 
279
- // New account (personal or company create_new)
280
- const accountName = useType === 'personal' ? fullName : companyName
281
- const newResult = await callInvitesApi('complete-registration', {
282
- path: 'new_account',
283
- email: resolvedEmail,
284
- account_name: accountName,
285
- role_slug: effectiveRole,
286
- }, 'POST')
287
-
288
- if (newResult.error) {
289
- setError(newResult.error)
295
+ // Handle account creation vs existing account assignment
296
+ let registrationResult
297
+ if (accountStrategy === 'existing' && targetAccount) {
298
+ // Assign to existing account
299
+ registrationResult = await callInvitesApi('complete-registration', {
300
+ path: 'existing_account',
301
+ email: resolvedEmail,
302
+ target_account: targetAccount,
303
+ role_slug: effectiveRole,
304
+ }, 'POST')
305
+ } else {
306
+ // Create new account (personal or company)
307
+ const accountName = useType === 'personal' ? fullName : companyName
308
+ registrationResult = await callInvitesApi('complete-registration', {
309
+ path: 'new_account',
310
+ email: resolvedEmail,
311
+ account_name: accountName,
312
+ role_slug: effectiveRole,
313
+ }, 'POST')
314
+ }
315
+
316
+ if (registrationResult.error) {
317
+ setError(registrationResult.error)
290
318
  return
291
319
  }
292
320
 
@@ -328,7 +356,7 @@ export function RegisterPage() {
328
356
  const isInvite = !!inviteToken && !!invitePreview
329
357
  const showDomainMatchPrompt = domainMatchAccountId && domainMatchChoice === null && useType === 'company' && !isFirstUser
330
358
  const effectiveEmail = isInvite ? invitePreview.email : email
331
- const showUseTypeToggle = !isInvite && !isFirstUser
359
+ const showUseTypeToggle = !isInvite && !isFirstUser && accountStrategy === 'new'
332
360
 
333
361
  // Header text based on scenario
334
362
  let headerTitle = 'Create your account'
@@ -481,8 +509,8 @@ export function RegisterPage() {
481
509
  </div>
482
510
  )}
483
511
 
484
- {/* Company name — shown for company type when not joining existing */}
485
- {!isInvite && useType === 'company' && domainMatchChoice !== 'join' && !isFirstUser && (
512
+ {/* Company name — shown for company type when not joining existing and using new account strategy */}
513
+ {!isInvite && useType === 'company' && domainMatchChoice !== 'join' && !isFirstUser && accountStrategy === 'new' && (
486
514
  <div>
487
515
  <label htmlFor="companyName" className="block text-sm font-medium text-slate-700">
488
516
  Company name
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spine-framework",
3
- "version": "0.3.60",
3
+ "version": "0.3.62",
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
  }