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
|
-
//
|
|
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:
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
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
|
}
|