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
|
|
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
|
-
|
|
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
|
-
//
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
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
|
}
|