import type { App, ObjectPlugin } from 'vue' import type { AccountInfo, User, NewUser, UpdatePasswordForm, UpdateAccountRequest, AuthEventMap, SSOProvider, SSOInitiateRequest, SSOCallbackRequest, SSOLinkRequest, TenantInfo, CreateTenantRequest, UpdateTenantRequest, AddMemberRequest, UpdateMemberRequest, CreateInvitationRequest, AcceptInvitationRequest, OTPSendRequest, OTPVerifyRequest, SMSSendRequest, SMSVerifyRequest, EmailTokenSendRequest, EmailTokenVerifyRequest, GenerateTokenRequest, GenerateTokenResponse, AuthenticationResponse, } from './types' import type { RedirectConfig, NormalizedRedirectConfig } from './types/redirect' import { ref, computed } from 'vue' import { AuthApi } from './api' import { setAuthContext, sso } from './sso' import { AuthState, accountToUser } from './types' import { normalizeRedirectConfig } from './types/redirect' import { EventEmitter } from './utils' // Global state let authApi: AuthApi | null = null let eventEmitter: EventEmitter | null = null let redirectConfig: NormalizedRedirectConfig | null = null let autoRedirectRouter: any = null // Router instance for auto-redirect let cachedAuthGuard: any = null // Cached router guard let tenancyEnabled = false const accountInfo = ref(null) const tenants = ref([]) const currentTenant = ref(null) interface InitParams { baseURL: string /** * Redirect configuration for authentication flows * @see RedirectConfig */ redirect?: RedirectConfig /** * Enable multi-tenancy support. When true, tenants are loaded after auth check. * @default false */ tenancy?: boolean } /** * Auth instance returned by createAuth * Implements ObjectPlugin for Vue 3.5+ compatibility */ export interface AuthInstance extends ObjectPlugin<[]> { on: (event: K, handler: AuthEventMap[K]) => void off: (event: K, handler: AuthEventMap[K]) => void removeAllListeners: (event?: K) => void use: (dependency: any, options?: { guard?: boolean }) => AuthInstance routerGuard: () => (to: any, from: any) => Promise install: (app: App) => void } /** * Get the current redirect configuration * Used internally by router guard */ export function getRedirectConfig(): NormalizedRedirectConfig { if (!redirectConfig) { throw new Error('Redirect config not initialized. Did you call createAuth with redirect config?') } return redirectConfig } // Initialize auth export function createAuth(params: InitParams): AuthInstance { if (authApi === null) { authApi = new AuthApi(params.baseURL) } if (eventEmitter === null) { eventEmitter = new EventEmitter() } // Store redirect configuration if (params.redirect) { redirectConfig = normalizeRedirectConfig(params.redirect) } tenancyEnabled = params.tenancy ?? false // Setup auto-redirect on login if enabled if (redirectConfig?.autoRedirect) { setupAutoRedirect() } const authInstance = { // Event listener methods on(event: K, handler: AuthEventMap[K]): void { if (eventEmitter) { eventEmitter.on(event, handler) } }, off(event: K, handler: AuthEventMap[K]): void { if (eventEmitter) { eventEmitter.off(event, handler) } }, removeAllListeners(event?: K): void { if (eventEmitter) { eventEmitter.removeAllListeners(event) } }, /** * Connect external dependencies like Vue Router * Automatically sets up router guard when router is provided * @param dependency - Vue Router instance or other plugins * @param options - Configuration options * @param options.guard - Whether to automatically set up auth guard (default: true) * @example * ```ts * // Auto setup (default) * auth.use(router) * * // Manual guard control (for custom composition) * auth.use(router, { guard: false }) * router.beforeEach(async (to, from) => { * // Custom logic first * if (!hasOrgAccess(to)) return '/no-access' * // Then run auth guard * return auth.routerGuard()(to, from) * }) * ``` */ use(dependency: any, options: { guard?: boolean } = {}) { const { guard = true } = options // Detect if it's a router by checking for common router properties if (dependency && (dependency.beforeEach || dependency.push || dependency.currentRoute)) { autoRedirectRouter = dependency // Automatically set up the auth guard unless disabled if (guard) { dependency.beforeEach(authInstance.routerGuard()) } } return authInstance }, /** * Create a Vue Router navigation guard for authentication * Protects routes requiring authentication and handles redirect logic * Note: Automatically called by auth.use(router), only use directly for custom setups * @example * ```ts * // Automatic (recommended) * auth.use(router) * * // Manual (for custom setups) * router.beforeEach(auth.routerGuard()) * ``` */ routerGuard() { // Return factory that lazily loads authGuard to avoid circular dependency if (cachedAuthGuard === null) { cachedAuthGuard = async (to: any, from: any) => { const { authGuard } = await import('./router') const guard = authGuard() // Cache the actual guard for next time cachedAuthGuard = guard return guard(to, from) } } return cachedAuthGuard }, /** * Vue plugin install method * Makes auth available globally as $auth * @example * ```ts * app.use(auth) * ``` */ install(app: App) { // Make auth available globally app.config.globalProperties.$auth = useAuth() }, } return authInstance } /** * Setup automatic redirect after successful login */ function setupAutoRedirect() { if (!eventEmitter || !redirectConfig) return eventEmitter.on(AuthState.LOGIN, async () => { // Only auto-redirect if router is available if (!autoRedirectRouter) { console.warn('[Auth] Auto-redirect enabled but router not set. Call auth.use(router) in your app setup.') return } // Dynamic import to avoid circular dependency const { performRedirect } = await import('./redirect') try { await performRedirect(autoRedirectRouter, redirectConfig!) } catch (error) { console.error('[Auth] Auto-redirect error:', error) } }) } // Composable export function useAuth() { if (authApi === null) { throw new Error('Auth not initialized. Call createAuth first.') } if (eventEmitter === null) { throw new Error('Event emitter not initialized. Call createAuth first.') } const api = authApi const emitter = eventEmitter // Set SSO auth context (allows sso.google.redirect() etc. to work) const authMethods = { initiateSSO: async (provider: SSOProvider, params: SSOInitiateRequest) => { const { data } = await api.initiateSSO(provider, params) return data.authorization_url }, loginWithSSO: async (provider: SSOProvider, params: SSOCallbackRequest) => { const { data } = await api.ssoCallback(provider, params) if (data.success === true && data.requires_verification !== true) { await checkAuth() } emitter.emit(AuthState.LOGIN) return data }, linkSSOProvider: async (provider: SSOProvider, params: SSOLinkRequest) => { await api.linkSSOProvider(provider, params) await checkAuth() }, unlinkSSOProvider: async (provider: SSOProvider) => { await api.unlinkSSOProvider(provider) await checkAuth() }, } setAuthContext(authMethods) // Computed user - the primary interface for accessing user data const user = computed(() => accountToUser(accountInfo.value)) // Getters const getFullName = () => { return user.value?.name ?? '' } const getIsLoggedIn = () => { return user.value !== null } const getEmail = () => { return user.value?.email ?? '' } const getRoles = () => { return user.value?.roles ?? [] } const getAccountType = () => { return user.value?.type ?? 'identity' } /** * Check if identity is linked to a person * @returns true if the identity has a person linked */ const isPersonAccount = () => { return user.value?.hasPersonLinked === true } // Actions async function logout() { // Call logout API const logoutPromise = api.logout() await logoutPromise.catch(() => { // Ignore logout errors }) // Clear local state regardless of API result accountInfo.value = null tenants.value = [] currentTenant.value = null api.setTenantId(null) // Emit logout event emitter.emit(AuthState.LOGOUT) } async function login(credentials: { email: string, password: string }) { const { data } = await api.login(credentials) // If successful and not requiring verification, fetch user data and tenants if (data.success === true && data.requires_verification !== true) { await checkAuth() // checkAuth now calls loadTenants automatically } emitter.emit(AuthState.LOGIN) return data } // ============================================ // SMS Login // ============================================ async function sendSMS(data: SMSSendRequest): Promise { const { data: response } = await api.sendSMS(data) return response } async function verifySMS(data: SMSVerifyRequest): Promise { const { data: response } = await api.verifySMS(data) if (response.success === true && response.requires_verification !== true) { await checkAuth() } emitter.emit(AuthState.LOGIN) return response } /** @deprecated Use sendSMS() instead */ async function sendOTP(data: OTPSendRequest): Promise { return sendSMS(data) } /** @deprecated Use verifySMS() instead */ async function verifyOTP(data: OTPVerifyRequest): Promise { return verifySMS(data) } // ============================================ // Email Token Login // ============================================ async function sendEmailToken(data: EmailTokenSendRequest): Promise { const { data: response } = await api.sendEmailToken(data) return response } async function verifyEmailToken(data: EmailTokenVerifyRequest): Promise { const { data: response } = await api.verifyEmailToken(data) if (response.success === true && response.requires_verification !== true) { await checkAuth() } emitter.emit(AuthState.LOGIN) return response } // ============================================ // Login Token (one-time token auth) // ============================================ /** * Generate a one-time login token. * Omit target_identity_id to generate for yourself (e.g. desktop → mobile). * Provide target_identity_id to generate for another user (admin only). */ async function generateLoginToken(data: GenerateTokenRequest = {}): Promise { const { data: response } = await api.generateLoginToken(data) return response } /** * Redeem a one-time login token and authenticate. * @param token - The login token string * @returns true if login succeeded, false otherwise */ async function loginWithToken(token: string): Promise { try { const { data: response } = await api.redeemLoginToken({ token }) if (response.success === true && response.requires_verification !== true) { await checkAuth() } if (response.success) { emitter.emit(AuthState.LOGIN) } return response.success === true } catch { return false } } async function checkAuth(): Promise { try { const { data } = await api.getCurrentUser() accountInfo.value = data if (getIsLoggedIn()) { emitter.emit(AuthState.AUTH_CHECK) if (tenancyEnabled) { await loadTenants().catch(() => {}) } } return true } catch { accountInfo.value = null return false } } async function loadTenants(): Promise { try { const { data } = await api.getTenants() tenants.value = data // Auto-select first tenant if none selected and tenants available if (currentTenant.value === null && tenants.value.length > 0) { const firstActiveTenant = tenants.value.find(t => t.status === 'active') if (firstActiveTenant !== undefined) { setTenant(firstActiveTenant.id) } } return tenants.value } catch { // If 404 or other error, assume multi-tenancy not supported tenants.value = [] return [] } } function setTenant(tenantId: string | null) { if (tenantId === null) { currentTenant.value = null api.setTenantId(null) return } const tenant = tenants.value.find(t => t.id === tenantId) if (tenant !== undefined) { currentTenant.value = tenant api.setTenantId(tenantId) } else { throw new Error(`Tenant with ID ${tenantId} not found`) } } function switchTenant(tenantId: string): void { setTenant(tenantId) } function getTenants() { return tenants.value } function getCurrentTenant() { return currentTenant.value } // ============================================ // Tenant CRUD // ============================================ async function createTenant(data: CreateTenantRequest) { const { data: tenant } = await api.createTenant(data) await loadTenants() return tenant } async function updateTenant(tenantId: string, data: UpdateTenantRequest) { const { data: tenant } = await api.updateTenant(tenantId, data) // Refresh list so currentTenant stays in sync await loadTenants() return tenant } async function deleteTenant(tenantId: string) { await api.deleteTenant(tenantId) if (currentTenant.value?.id === tenantId) { currentTenant.value = null api.setTenantId(null) } await loadTenants() } // ============================================ // Tenant Members // ============================================ async function getTenantMembers(tenantId: string, status?: string) { const { data } = await api.getTenantMembers(tenantId, status) return data } async function addTenantMember(tenantId: string, member: AddMemberRequest) { const { data } = await api.addTenantMember(tenantId, member) return data } async function updateTenantMember(tenantId: string, identityId: string, updates: UpdateMemberRequest) { const { data } = await api.updateTenantMember(tenantId, identityId, updates) return data } async function activateTenantMember(tenantId: string, identityId: string) { const { data } = await api.activateTenantMember(tenantId, identityId) return data } async function removeTenantMember(tenantId: string, identityId: string) { await api.removeTenantMember(tenantId, identityId) } async function getTenantRoles() { const { data } = await api.getTenantRoles() return data } // ============================================ // Invitations // ============================================ async function createInvitation(data: CreateInvitationRequest) { const { data: invitation } = await api.createInvitation(data) return invitation } async function getInvitation(token: string) { const { data } = await api.getInvitation(token) return data } async function acceptInvitation(token: string, data?: AcceptInvitationRequest) { await api.acceptInvitation(token, data) // Re-check auth so tenants are reloaded after membership is created await checkAuth() } async function signup(newUser: NewUser) { // Check password match if password is provided const hasPassword = newUser.password !== undefined && newUser.password.length > 0 if (hasPassword && newUser.password !== newUser.confirmPassword) { throw new Error('Passwords do not match') } const { data } = await api.register({ email: newUser.email, first_name: newUser.first_name, last_name: newUser.last_name, phone_number: newUser.phone_number, password: newUser.password, }) // If successful and not requiring verification, fetch user data if (data.success === true && data.requires_verification !== true) { await checkAuth() } emitter.emit(AuthState.SIGNUP) return data } async function forgotPassword(email: string) { await api.forgotPassword({ email }) } async function verifyResetToken(token: string) { await api.verifyResetToken(token) } async function resetPassword(token: string, newPassword: string) { await api.resetPassword({ token, new_password: newPassword }) emitter.emit(AuthState.PASSWORD_RESET) } async function changePassword(form: UpdatePasswordForm) { if (form.new_password !== form.confirmNewPassword) { throw new Error('Passwords do not match') } await api.changePassword({ current_password: form.current_password, new_password: form.new_password, }) emitter.emit(AuthState.PASSWORD_CHANGE) } async function updateProfile(updates: UpdateAccountRequest) { const { data } = await api.updateCurrentUser(updates) accountInfo.value = data emitter.emit(AuthState.PROFILE_UPDATE) } async function deleteCurrentUser() { await api.deleteCurrentUser() accountInfo.value = null } async function sendVerification(email?: string) { await api.sendVerification({ email }) } async function verifyEmail(token: string) { await api.verifyEmail({ token }) // Refresh user data to get updated verification status await checkAuth() emitter.emit(AuthState.EMAIL_VERIFIED) } async function refreshSession() { await api.refreshSession() emitter.emit(AuthState.SESSION_REFRESH) } async function getSessions() { return api.getSessions() } async function revokeSession(sessionToken: string) { await api.revokeSession(sessionToken) } async function revokeAllSessions() { await api.revokeAllSessions() } // ============================================ // SSO Authentication Methods // ============================================ /** * Initiate SSO login flow * @returns Authorization URL to redirect user to */ async function initiateSSO(provider: SSOProvider, params: SSOInitiateRequest) { const { data } = await api.initiateSSO(provider, params) return data.authorization_url } /** * Complete SSO login after callback from provider */ async function loginWithSSO(provider: SSOProvider, params: SSOCallbackRequest) { const { data } = await api.ssoCallback(provider, params) if (data.success === true && data.requires_verification !== true) { await checkAuth() } emitter.emit(AuthState.LOGIN) return data } /** * Link an SSO provider to the current account */ async function linkSSOProvider(provider: SSOProvider, params: SSOLinkRequest) { await api.linkSSOProvider(provider, params) await checkAuth() } /** * Unlink an SSO provider from the current account */ async function unlinkSSOProvider(provider: SSOProvider) { await api.unlinkSSOProvider(provider) await checkAuth() } return { // Primary State (use this!) user, // Full account info (for advanced use cases) accountInfo, // SSO Providers (ready to use!) sso, // Multi-Tenancy State tenants, currentTenant, // Getters getFullName, getIsLoggedIn, getEmail, getRoles, getAccountType, isPersonAccount, getTenants, getCurrentTenant, // Authentication Actions login, logout, signup, checkAuth, refreshSession, // SMS Login sendSMS, verifySMS, // SMS Login (deprecated aliases) sendOTP, verifyOTP, // Email Token Login sendEmailToken, verifyEmailToken, // Login Token (one-time token auth) generateLoginToken, loginWithToken, // SSO Authentication (lower-level - prefer using sso.google.redirect()) initiateSSO, loginWithSSO, linkSSOProvider, unlinkSSOProvider, // Profile Actions updateProfile, deleteCurrentUser, // Password Actions changePassword, forgotPassword, verifyResetToken, resetPassword, // Email Verification Actions sendVerification, verifyEmail, // Session Management getSessions, revokeSession, revokeAllSessions, // Multi-Tenancy Actions loadTenants, setTenant, switchTenant, // Tenant CRUD (requires tenancy: true) createTenant, updateTenant, deleteTenant, // Tenant Members getTenantMembers, addTenantMember, updateTenantMember, activateTenantMember, removeTenantMember, getTenantRoles, // Invitations createInvitation, getInvitation, acceptInvitation, // Axios integration registerAxios: (instance: any) => api.registerAxios(instance), } }