@bagelink/auth 1.7.94 → 1.7.98

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.
package/src/api.ts CHANGED
@@ -37,17 +37,33 @@ import type {
37
37
  SSOCallbackResponse,
38
38
  SSOLinkResponse,
39
39
  SSOUnlinkResponse,
40
+ GetTenantsResponse,
40
41
  } from './types'
41
42
  import { createAxiosInstance } from './utils'
42
43
 
43
44
  export class AuthApi {
44
45
  private api: AxiosInstance
46
+ private currentTenantId: string | null = null
45
47
 
46
48
  constructor(baseURL: string = '') {
47
49
  this.api = createAxiosInstance(baseURL)
48
50
  this.setupInterceptors()
49
51
  }
50
52
 
53
+ /**
54
+ * Set the current tenant ID for multi-tenant requests
55
+ */
56
+ setTenantId(tenantId: string | null) {
57
+ this.currentTenantId = tenantId
58
+ }
59
+
60
+ /**
61
+ * Get the current tenant ID
62
+ */
63
+ getTenantId(): string | null {
64
+ return this.currentTenantId
65
+ }
66
+
51
67
  private setupInterceptors() {
52
68
  this.api.interceptors.request.use((config: InternalAxiosRequestConfig) => {
53
69
  // Handle password reset token from URL
@@ -56,6 +72,12 @@ export class AuthApi {
56
72
  if (resetToken !== null) {
57
73
  config.headers['X-Reset-Token'] = resetToken
58
74
  }
75
+
76
+ // Add tenant ID header if set
77
+ if (this.currentTenantId !== null) {
78
+ config.headers['X-Tenant-ID'] = this.currentTenantId
79
+ }
80
+
59
81
  return config
60
82
  })
61
83
  }
@@ -68,14 +90,14 @@ export class AuthApi {
68
90
  * Get available authentication methods
69
91
  */
70
92
  async getAuthMethods(): Promise<GetMethodsResponse> {
71
- return this.api.get('/authentication/methods')
93
+ return this.api.get('authentication/methods')
72
94
  }
73
95
 
74
96
  /**
75
97
  * Register a new account
76
98
  */
77
99
  async register(data: RegisterRequest): Promise<RegisterResponse> {
78
- return this.api.post('/authentication/register', {
100
+ return this.api.post('authentication/register', {
79
101
  ...data,
80
102
  email: data.email.toLowerCase(),
81
103
  })
@@ -85,7 +107,7 @@ export class AuthApi {
85
107
  * Login with password
86
108
  */
87
109
  async login(email: string, password: string): Promise<LoginResponse> {
88
- return this.api.post('/authentication/login/password', {
110
+ return this.api.post('authentication/login/password', {
89
111
  email: email.toLowerCase(),
90
112
  password,
91
113
  })
@@ -95,14 +117,14 @@ export class AuthApi {
95
117
  * Logout and clear session
96
118
  */
97
119
  async logout(): Promise<LogoutResponse> {
98
- return this.api.post('/authentication/logout', {})
120
+ return this.api.post('authentication/logout', {})
99
121
  }
100
122
 
101
123
  /**
102
124
  * Refresh current session
103
125
  */
104
126
  async refreshSession(): Promise<RefreshSessionResponse> {
105
- return this.api.post('/authentication/refresh', {})
127
+ return this.api.post('authentication/refresh', {})
106
128
  }
107
129
 
108
130
  // ============================================
@@ -114,7 +136,7 @@ export class AuthApi {
114
136
  * Returns authorization URL to redirect user to
115
137
  */
116
138
  async initiateSSO(data: SSOInitiateRequest): Promise<SSOInitiateResponse> {
117
- return this.api.post(`/authentication/sso/${data.provider}/initiate`, {
139
+ return this.api.post(`authentication/sso/${data.provider}/initiate`, {
118
140
  redirect_uri: data.redirect_uri,
119
141
  state: data.state,
120
142
  })
@@ -124,7 +146,7 @@ export class AuthApi {
124
146
  * Complete SSO login after callback from provider
125
147
  */
126
148
  async ssoCallback(data: SSOCallbackRequest): Promise<SSOCallbackResponse> {
127
- return this.api.post(`/authentication/sso/${data.provider}/callback`, {
149
+ return this.api.post(`authentication/sso/${data.provider}/callback`, {
128
150
  code: data.code,
129
151
  state: data.state,
130
152
  })
@@ -134,7 +156,7 @@ export class AuthApi {
134
156
  * Link an SSO provider to existing account
135
157
  */
136
158
  async linkSSOProvider(data: SSOLinkRequest): Promise<SSOLinkResponse> {
137
- return this.api.post(`/authentication/sso/${data.provider}/link`, {
159
+ return this.api.post(`authentication/sso/${data.provider}/link`, {
138
160
  code: data.code,
139
161
  state: data.state,
140
162
  })
@@ -144,7 +166,7 @@ export class AuthApi {
144
166
  * Unlink an SSO provider from account
145
167
  */
146
168
  async unlinkSSOProvider(provider: SSOProvider): Promise<SSOUnlinkResponse> {
147
- return this.api.delete(`/authentication/sso/${provider}/unlink`)
169
+ return this.api.delete(`authentication/sso/${provider}/unlink`)
148
170
  }
149
171
 
150
172
  // ============================================
@@ -155,21 +177,21 @@ export class AuthApi {
155
177
  * Get current user account info
156
178
  */
157
179
  async getCurrentUser(): Promise<GetMeResponse> {
158
- return this.api.get('/authentication/me')
180
+ return this.api.get('authentication/me')
159
181
  }
160
182
 
161
183
  /**
162
184
  * Update current user profile
163
185
  */
164
186
  async updateCurrentUser(data: UpdateAccountRequest): Promise<UpdateMeResponse> {
165
- return this.api.patch('/authentication/me', data)
187
+ return this.api.patch('authentication/me', data)
166
188
  }
167
189
 
168
190
  /**
169
191
  * Delete current user account
170
192
  */
171
193
  async deleteCurrentUser(): Promise<DeleteMeResponse> {
172
- return this.api.delete('/authentication/me')
194
+ return this.api.delete('authentication/me')
173
195
  }
174
196
 
175
197
  // ============================================
@@ -180,7 +202,7 @@ export class AuthApi {
180
202
  * Get account information by ID
181
203
  */
182
204
  async getAccount(accountId: string): Promise<GetAccountResponse> {
183
- return this.api.get(`/authentication/account/${accountId}`)
205
+ return this.api.get(`authentication/account/${accountId}`)
184
206
  }
185
207
 
186
208
  /**
@@ -190,21 +212,21 @@ export class AuthApi {
190
212
  accountId: string,
191
213
  data: UpdateAccountRequest
192
214
  ): Promise<UpdateAccountResponse> {
193
- return this.api.patch(`/authentication/account/${accountId}`, data)
215
+ return this.api.patch(`authentication/account/${accountId}`, data)
194
216
  }
195
217
 
196
218
  /**
197
219
  * Delete account by ID
198
220
  */
199
221
  async deleteAccount(accountId: string): Promise<DeleteAccountResponse> {
200
- return this.api.delete(`/authentication/account/${accountId}`)
222
+ return this.api.delete(`authentication/account/${accountId}`)
201
223
  }
202
224
 
203
225
  /**
204
226
  * Activate account by ID
205
227
  */
206
228
  async activateAccount(accountId: string): Promise<ActivateAccountResponse> {
207
- return this.api.post(`/authentication/account/${accountId}/activate`, {})
229
+ return this.api.post(`authentication/account/${accountId}/activate`, {})
208
230
  }
209
231
 
210
232
  /**
@@ -213,7 +235,7 @@ export class AuthApi {
213
235
  async deactivateAccount(
214
236
  accountId: string
215
237
  ): Promise<DeactivateAccountResponse> {
216
- return this.api.post(`/authentication/account/${accountId}/deactivate`, {})
238
+ return this.api.post(`authentication/account/${accountId}/deactivate`, {})
217
239
  }
218
240
 
219
241
  // ============================================
@@ -224,14 +246,14 @@ export class AuthApi {
224
246
  * Change password (requires current password)
225
247
  */
226
248
  async changePassword(data: ChangePasswordRequest): Promise<ChangePasswordResponse> {
227
- return this.api.post('/authentication/password/change', data)
249
+ return this.api.post('authentication/password/change', data)
228
250
  }
229
251
 
230
252
  /**
231
253
  * Initiate forgot password flow
232
254
  */
233
255
  async forgotPassword(email: string): Promise<ForgotPasswordResponse> {
234
- return this.api.post('/authentication/password/forgot', {
256
+ return this.api.post('authentication/password/forgot', {
235
257
  email: email.toLowerCase(),
236
258
  })
237
259
  }
@@ -240,14 +262,14 @@ export class AuthApi {
240
262
  * Verify password reset token
241
263
  */
242
264
  async verifyResetToken(token: string): Promise<VerifyResetTokenResponse> {
243
- return this.api.get(`/authentication/password/verify-reset-token/${token}`)
265
+ return this.api.get(`authentication/password/verify-reset-token/${token}`)
244
266
  }
245
267
 
246
268
  /**
247
269
  * Reset password with token
248
270
  */
249
271
  async resetPassword(data: ResetPasswordRequest): Promise<ResetPasswordResponse> {
250
- return this.api.post('/authentication/password/reset', data)
272
+ return this.api.post('authentication/password/reset', data)
251
273
  }
252
274
 
253
275
  // ============================================
@@ -261,7 +283,7 @@ export class AuthApi {
261
283
  data: SendVerificationRequest = {},
262
284
  user?: AuthenticationAccount
263
285
  ): Promise<SendVerificationResponse> {
264
- return this.api.post('/authentication/verify/send', data, {
286
+ return this.api.post('authentication/verify/send', data, {
265
287
  params: user ? { user } : undefined,
266
288
  })
267
289
  }
@@ -270,7 +292,7 @@ export class AuthApi {
270
292
  * Verify email with token
271
293
  */
272
294
  async verifyEmail(token: string): Promise<VerifyEmailResponse> {
273
- return this.api.post('/authentication/verify/email', { token })
295
+ return this.api.post('authentication/verify/email', { token })
274
296
  }
275
297
 
276
298
  // ============================================
@@ -281,27 +303,38 @@ export class AuthApi {
281
303
  * Get sessions for an account
282
304
  */
283
305
  async getSessions(accountId: string): Promise<GetSessionsResponse> {
284
- return this.api.get(`/authentication/sessions/${accountId}`)
306
+ return this.api.get(`authentication/sessions/${accountId}`)
285
307
  }
286
308
 
287
309
  /**
288
310
  * Revoke a specific session
289
311
  */
290
312
  async revokeSession(sessionToken: string): Promise<DeleteSessionResponse> {
291
- return this.api.delete(`/authentication/sessions/${sessionToken}`)
313
+ return this.api.delete(`authentication/sessions/${sessionToken}`)
292
314
  }
293
315
 
294
316
  /**
295
317
  * Revoke all sessions for an account
296
318
  */
297
319
  async revokeAllSessions(accountId: string): Promise<DeleteAllSessionsResponse> {
298
- return this.api.delete(`/authentication/sessions/account/${accountId}`)
320
+ return this.api.delete(`authentication/sessions/account/${accountId}`)
299
321
  }
300
322
 
301
323
  /**
302
324
  * Cleanup expired sessions (admin)
303
325
  */
304
326
  async cleanupSessions(): Promise<CleanupSessionsResponse> {
305
- return this.api.post('/authentication/cleanup-sessions', {})
327
+ return this.api.post('authentication/cleanup-sessions', {})
328
+ }
329
+
330
+ // ============================================
331
+ // Multi-Tenancy Methods
332
+ // ============================================
333
+
334
+ /**
335
+ * Get list of tenants the authenticated user belongs to
336
+ */
337
+ async getTenants(): Promise<GetTenantsResponse> {
338
+ return this.api.get('tenants')
306
339
  }
307
340
  }
package/src/types.ts CHANGED
@@ -34,13 +34,33 @@ export interface AuthEventMap {
34
34
  // Core Types
35
35
  // ============================================
36
36
 
37
- export type AuthenticationAccountType = 'person' | 'entity' | 'service'
37
+ // Note: All accounts are "identity" accounts. An identity may be linked to a person or not.
38
+ export type AuthenticationAccountType = 'identity' | string
38
39
 
39
- export type AuthenticationMethodType =
40
- | 'password'
41
- | 'email_token'
42
- | 'sso'
43
- | 'otp'
40
+ // ============================================
41
+ // Multi-Tenancy Types
42
+ // ============================================
43
+
44
+ export type TenantStatus = 'active' | 'suspended' | 'archived'
45
+
46
+ export interface TenantInfo {
47
+ id: string
48
+ name: string
49
+ slug: string
50
+ parent_id: string | null
51
+ settings: Record<string, any> | null
52
+ status: TenantStatus
53
+ suspended_at: string | null
54
+ suspended_reason: string | null
55
+ created_at: string
56
+ updated_at: string
57
+ }
58
+
59
+ export type AuthenticationMethodType
60
+ = | 'password'
61
+ | 'email_token'
62
+ | 'sso'
63
+ | 'otp'
44
64
 
45
65
  export type SSOProvider = 'google' | 'microsoft' | 'github' | 'okta' | 'apple' | 'facebook'
46
66
 
@@ -63,11 +83,11 @@ export interface AuthenticationAccount {
63
83
  export interface PersonInfo {
64
84
  id: string
65
85
  name: string
66
- email?: string
67
- phone?: string
68
- roles: string[]
69
86
  first_name: string
70
87
  last_name: string
88
+ email?: string | null
89
+ phone_number?: string | null
90
+ roles: string[]
71
91
  }
72
92
 
73
93
  export interface AuthMethodInfo {
@@ -87,12 +107,13 @@ export interface AccountInfo {
87
107
  display_name: string
88
108
  is_active: boolean
89
109
  is_verified: boolean
90
- last_login?: string
110
+ last_login?: string | null
91
111
  authentication_methods: AuthMethodInfo[]
92
- person?: PersonInfo
93
- entity?: EntityInfo
112
+ person?: PersonInfo | null
94
113
  }
95
114
 
115
+ // Note: EntityInfo kept for backward compatibility
116
+ // Current API no longer returns entity in AccountInfo
96
117
  export interface EntityInfo {
97
118
  id: string
98
119
  name: string
@@ -114,21 +135,21 @@ export interface SessionInfo {
114
135
  // ============================================
115
136
 
116
137
  /**
117
- * Unified user representation that works for both person and entity accounts
118
- * This is the primary interface for accessing user data in the application
138
+ * Unified user representation
139
+ * All accounts are "identity" accounts that may be linked to a person
119
140
  */
120
141
  export interface User {
121
- /** Unique identifier (person_id or entity_id) */
142
+ /** Unique identifier (person_id if linked, otherwise identity_id) */
122
143
  id: string
123
- /** Account ID */
144
+ /** Identity/Account ID */
124
145
  accountId: string
125
146
  /** Display name */
126
147
  name: string
127
148
  /** Email address (from person or authentication methods) */
128
149
  email?: string
129
- /** Account type: 'person', 'entity', or 'service' */
150
+ /** Account type (always 'identity') */
130
151
  type: AuthenticationAccountType
131
- /** User roles (only for person accounts) */
152
+ /** User roles (only when linked to person) */
132
153
  roles?: string[]
133
154
  /** Is the account active */
134
155
  isActive: boolean
@@ -136,12 +157,10 @@ export interface User {
136
157
  isVerified: boolean
137
158
  /** Last login timestamp */
138
159
  lastLogin?: string
139
- /** Entity-specific info (only for entity accounts) */
140
- entityType?: string
141
- /** Additional metadata */
142
- metadata?: Record<string, any>
143
- /** Person-specific info (only for person accounts) */
160
+ /** Person info (if identity is linked to a person) */
144
161
  person?: PersonInfo
162
+ /** Whether identity is linked to a person */
163
+ hasPersonLinked: boolean
145
164
  }
146
165
 
147
166
  // ============================================
@@ -302,6 +321,7 @@ export type SSOInitiateResponse = AxiosResponse<{ authorization_url: string }>
302
321
  export type SSOCallbackResponse = AxiosResponse<AuthenticationResponse>
303
322
  export type SSOLinkResponse = AxiosResponse<MessageResponse>
304
323
  export type SSOUnlinkResponse = AxiosResponse<MessageResponse>
324
+ export type GetTenantsResponse = AxiosResponse<TenantInfo[]>
305
325
 
306
326
  // ============================================
307
327
  // Helper Functions (exported for convenience)
@@ -309,45 +329,33 @@ export type SSOUnlinkResponse = AxiosResponse<MessageResponse>
309
329
 
310
330
  /**
311
331
  * Extract unified user from account info
332
+ * All accounts are identities that may be linked to a person
312
333
  */
313
334
  export function accountToUser(account: AccountInfo | null): User | null {
314
335
  if (account === null) { return null }
315
336
 
316
- // Person account - most common case
317
- if (account.person !== undefined) {
318
- return {
319
- id: account.person.id,
320
- accountId: account.id,
321
- name: account.person.name,
322
- email: account.person.email,
323
- type: account.account_type as AuthenticationAccountType,
324
- roles: account.person.roles,
325
- isActive: account.is_active,
326
- isVerified: account.is_verified,
327
- person: account.person,
328
- lastLogin: account.last_login,
329
- }
330
- }
337
+ const hasPersonLinked = account.person !== undefined && account.person !== null
331
338
 
332
- // Entity account
333
- if (account.entity !== undefined) {
339
+ // Identity linked to person - use person data
340
+ if (hasPersonLinked) {
334
341
  return {
335
- id: account.entity.id,
342
+ id: account.person!.id,
336
343
  accountId: account.id,
337
- name: account.entity.name,
344
+ name: account.person!.name,
345
+ email: account.person!.email ?? undefined,
338
346
  type: account.account_type as AuthenticationAccountType,
347
+ roles: account.person!.roles,
339
348
  isActive: account.is_active,
340
349
  isVerified: account.is_verified,
341
- lastLogin: account.last_login,
342
- entityType: account.entity.type,
343
- metadata: account.entity.metadata,
350
+ person: account.person!,
351
+ lastLogin: account.last_login ?? undefined,
352
+ hasPersonLinked: true,
344
353
  }
345
354
  }
346
355
 
347
- // Fallback - use account info directly
348
- // Extract email from authentication methods
356
+ // Identity without person link - extract data from authentication methods
349
357
  const emailMethod = account.authentication_methods.find(
350
- m => m.type === 'password' || m.type === 'email_token',
358
+ m => m.type === 'password' || m.type === 'email_token' || m.type === 'sso',
351
359
  )
352
360
 
353
361
  return {
@@ -358,6 +366,7 @@ export function accountToUser(account: AccountInfo | null): User | null {
358
366
  type: account.account_type as AuthenticationAccountType,
359
367
  isActive: account.is_active,
360
368
  isVerified: account.is_verified,
361
- lastLogin: account.last_login,
369
+ lastLogin: account.last_login ?? undefined,
370
+ hasPersonLinked: false,
362
371
  }
363
372
  }
package/src/useAuth.ts CHANGED
@@ -10,6 +10,7 @@ import type {
10
10
  SSOInitiateRequest,
11
11
  SSOCallbackRequest,
12
12
  SSOLinkRequest,
13
+ TenantInfo,
13
14
  } from './types'
14
15
  import type { RedirectConfig, NormalizedRedirectConfig } from './types/redirect'
15
16
  import { ref, computed } from 'vue'
@@ -26,6 +27,8 @@ let redirectConfig: NormalizedRedirectConfig | null = null
26
27
  let autoRedirectRouter: any = null // Router instance for auto-redirect
27
28
  let cachedAuthGuard: any = null // Cached router guard
28
29
  const accountInfo = ref<AccountInfo | null>(null)
30
+ const tenants = ref<TenantInfo[]>([])
31
+ const currentTenant = ref<TenantInfo | null>(null)
29
32
 
30
33
  interface InitParams {
31
34
  baseURL: string
@@ -262,17 +265,16 @@ export function useAuth() {
262
265
  }
263
266
 
264
267
  const getAccountType = () => {
265
- return user.value?.type ?? 'person'
268
+ return user.value?.type ?? 'identity'
266
269
  }
267
270
 
271
+ /**
272
+ * Check if identity is linked to a person
273
+ * @returns true if the identity has a person linked
274
+ */
268
275
  const isPersonAccount = () => {
269
- return user.value?.type === 'person'
270
- }
271
-
272
- const isEntityAccount = () => {
273
- return user.value?.type === 'entity'
276
+ return user.value?.hasPersonLinked === true
274
277
  }
275
-
276
278
  // Actions
277
279
  async function logout() {
278
280
  // Call logout API
@@ -315,6 +317,55 @@ export function useAuth() {
315
317
  }
316
318
  }
317
319
 
320
+ async function loadTenants(): Promise<TenantInfo[]> {
321
+ try {
322
+ const { data } = await api.getTenants()
323
+ tenants.value = data
324
+
325
+ // Auto-select first tenant if none selected and tenants available
326
+ if (currentTenant.value === null && tenants.value.length > 0) {
327
+ const firstActiveTenant = tenants.value.find(t => t.status === 'active')
328
+ if (firstActiveTenant !== undefined) {
329
+ setTenant(firstActiveTenant.id)
330
+ }
331
+ }
332
+
333
+ return tenants.value
334
+ } catch {
335
+ // If 404 or other error, assume multi-tenancy not supported
336
+ tenants.value = []
337
+ return []
338
+ }
339
+ }
340
+
341
+ function setTenant(tenantId: string | null) {
342
+ if (tenantId === null) {
343
+ currentTenant.value = null
344
+ api.setTenantId(null)
345
+ return
346
+ }
347
+
348
+ const tenant = tenants.value.find(t => t.id === tenantId)
349
+ if (tenant !== undefined) {
350
+ currentTenant.value = tenant
351
+ api.setTenantId(tenantId)
352
+ } else {
353
+ throw new Error(`Tenant with ID ${tenantId} not found`)
354
+ }
355
+ }
356
+
357
+ function switchTenant(tenantId: string): void {
358
+ setTenant(tenantId)
359
+ }
360
+
361
+ function getTenants() {
362
+ return tenants.value
363
+ }
364
+
365
+ function getCurrentTenant() {
366
+ return currentTenant.value
367
+ }
368
+
318
369
  async function signup(newUser: NewUser) {
319
370
  // Check password match if password is provided
320
371
  const hasPassword = newUser.password !== undefined && newUser.password.length > 0
@@ -478,6 +529,10 @@ export function useAuth() {
478
529
  // SSO Providers (ready to use!)
479
530
  sso,
480
531
 
532
+ // Multi-Tenancy State
533
+ tenants,
534
+ currentTenant,
535
+
481
536
  // Getters
482
537
  getFullName,
483
538
  getIsLoggedIn,
@@ -485,7 +540,8 @@ export function useAuth() {
485
540
  getRoles,
486
541
  getAccountType,
487
542
  isPersonAccount,
488
- isEntityAccount,
543
+ getTenants,
544
+ getCurrentTenant,
489
545
 
490
546
  // Authentication Actions
491
547
  login,
@@ -523,5 +579,10 @@ export function useAuth() {
523
579
  getSessions,
524
580
  revokeSession,
525
581
  revokeAllSessions,
582
+
583
+ // Multi-Tenancy Actions
584
+ loadTenants,
585
+ setTenant,
586
+ switchTenant,
526
587
  }
527
588
  }