@bagelink/auth 1.7.72 → 1.7.76

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@bagelink/auth",
3
3
  "type": "module",
4
- "version": "1.7.72",
4
+ "version": "1.7.76",
5
5
  "description": "Bagelink auth package",
6
6
  "author": {
7
7
  "name": "Bagel Studio",
package/src/index.ts CHANGED
@@ -14,17 +14,25 @@ export { default as ForgotPasswordPage } from './pages/ForgotPasswordPage.vue'
14
14
  export { default as LoginPage } from './pages/LoginPage.vue'
15
15
  export { default as ResetPasswordPage } from './pages/ResetPasswordPage.vue'
16
16
  export { default as SignupPage } from './pages/SignupPage.vue'
17
+
18
+ // Redirect utilities
19
+ export * from './redirect'
20
+
21
+ // Router integration
22
+ export * from './router'
23
+
17
24
  // Routes
18
25
  export * from './routes'
19
26
  export * from './sso'
20
27
 
21
- // Types (re-export from types file and module)
28
+ // Types ( e-export fro types file and modu e)
22
29
  export * from './types'
23
-
24
30
  export type {
25
31
  ForgotPasswordTexts,
26
32
  LoginTexts,
27
33
  ResetPasswordTexts,
28
34
  SignupTexts,
29
35
  } from './types/'
36
+
37
+ export * from './types/redirect'
30
38
  export * from './useAuth'
@@ -0,0 +1,95 @@
1
+ import type { Router } from 'vue-router'
2
+ import type { NormalizedRedirectConfig } from './types/redirect'
3
+
4
+ /**
5
+ * Redirect utilities for handling post-authentication navigation
6
+ */
7
+
8
+ /**
9
+ * Get redirect URL from current route query params
10
+ */
11
+ export function getRedirectUrl(
12
+ router: Router,
13
+ config: NormalizedRedirectConfig,
14
+ ): string {
15
+ const redirect = router.currentRoute.value.query[config.queryKey] as string
16
+ return redirect || config.fallback
17
+ }
18
+
19
+ /**
20
+ * Validate redirect URL is safe (prevents open redirect attacks)
21
+ */
22
+ export function isValidRedirect(
23
+ redirectUrl: string,
24
+ allowedPaths?: RegExp[],
25
+ ): boolean {
26
+ // Null or empty check
27
+ if (!redirectUrl) {
28
+ return false
29
+ }
30
+
31
+ // Prevent redirects to external URLs (absolute URLs with protocol)
32
+ if (redirectUrl.startsWith('http://') || redirectUrl.startsWith('https://')) {
33
+ return false
34
+ }
35
+
36
+ // Prevent protocol-relative URLs
37
+ if (redirectUrl.startsWith('//')) {
38
+ return false
39
+ }
40
+
41
+ // Prevent javascript: and data: URLs
42
+ if (redirectUrl.startsWith('javascript:') || redirectUrl.startsWith('data:')) {
43
+ return false
44
+ }
45
+
46
+ // Ensure it starts with /
47
+ if (!redirectUrl.startsWith('/')) {
48
+ return false
49
+ }
50
+
51
+ // If allowed paths are specified, check against them
52
+ if (allowedPaths && allowedPaths.length > 0) {
53
+ return allowedPaths.some(pattern => pattern.test(redirectUrl))
54
+ }
55
+
56
+ return true
57
+ }
58
+
59
+ /**
60
+ * Perform redirect after login with security validation
61
+ */
62
+ export async function performRedirect(
63
+ router: Router,
64
+ config: NormalizedRedirectConfig,
65
+ ): Promise<void> {
66
+ const redirect = getRedirectUrl(router, config)
67
+
68
+ // Always validate redirect URL for security
69
+ if (redirect !== config.fallback && !isValidRedirect(redirect, config.allowedPaths)) {
70
+ console.warn('[Auth] Invalid redirect URL detected, using fallback:', redirect)
71
+ await router.push(config.fallback)
72
+ return
73
+ }
74
+
75
+ await router.push(redirect)
76
+ }
77
+
78
+ /**
79
+ * Build query params for redirect to login
80
+ */
81
+ export function buildLoginQuery(
82
+ currentPath: string,
83
+ config: NormalizedRedirectConfig,
84
+ ): Record<string, string> {
85
+ if (!config.preserveRedirect) {
86
+ return {}
87
+ }
88
+
89
+ // Validate the current path before storing it
90
+ if (isValidRedirect(currentPath, config.allowedPaths)) {
91
+ return { [config.queryKey]: currentPath }
92
+ }
93
+
94
+ return {}
95
+ }
package/src/router.ts ADDED
@@ -0,0 +1,129 @@
1
+ import type { NavigationGuard, NavigationGuardNext, RouteLocationNormalized } from 'vue-router'
2
+ import { buildLoginQuery } from './redirect'
3
+ import { useAuth, getRedirectConfig } from './useAuth'
4
+
5
+ /**
6
+ * Auth router integration for Vue Router
7
+ */
8
+
9
+ // Track if auth has been initialized (for performance)
10
+ let authInitialized = false
11
+
12
+ /**
13
+ * Reset auth initialization state
14
+ * Useful for testing or app reload scenarios
15
+ */
16
+ export function resetAuthState() {
17
+ authInitialized = false
18
+ }
19
+
20
+ /**
21
+ * Auth guard for Vue Router
22
+ *
23
+ * Protects routes requiring authentication and handles redirect logic.
24
+ * Reads configuration from the auth instance created via createAuth().
25
+ *
26
+ * @example
27
+ * ```ts
28
+ * const auth = createAuth({
29
+ * baseURL: 'https://api.example.com',
30
+ * redirect: { ... }
31
+ * })
32
+ * router.beforeEach(authGuard())
33
+ * ```
34
+ */
35
+ export function authGuard() {
36
+ return async function guard(
37
+
38
+ to: RouteLocationNormalized,
39
+ _from: RouteLocationNormalized,
40
+ next: NavigationGuardNext,
41
+ ): Promise<void> {
42
+ const auth = useAuth()
43
+ const config = getRedirectConfig()
44
+
45
+ // Check if route requires authentication
46
+ const requiresAuth = to.meta[config.authMetaKey]
47
+
48
+ // Check if route is an auth-only page (login, signup, etc)
49
+ const requiresNoAuth = config.noAuthRoutes.includes(to.name as string)
50
+
51
+ try {
52
+ // Only check auth once on first navigation for performance
53
+ if (!authInitialized) {
54
+ await auth.checkAuth()
55
+ authInitialized = true
56
+ }
57
+
58
+ // Use cached auth state from reactive user ref
59
+ const isAuthenticated = !!auth.user.value
60
+
61
+ // Redirect authenticated users away from auth pages
62
+ if (isAuthenticated && requiresNoAuth) {
63
+ next(config.authenticatedRedirect)
64
+ return
65
+ }
66
+
67
+ // Redirect unauthenticated users to login for protected pages
68
+ if (!isAuthenticated && requiresAuth) {
69
+ const query = buildLoginQuery(to.fullPath, config)
70
+
71
+ next({
72
+ name: config.loginRoute,
73
+ query,
74
+ })
75
+ return
76
+ }
77
+
78
+ // Allow navigation
79
+ next()
80
+ } catch (error) {
81
+ console.error('[Auth Guard] Error:', error)
82
+ // On error, allow navigation but log the issue
83
+ next()
84
+ }
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Compose multiple navigation guards into one
90
+ * Guards are executed in order, stopping at the first one that calls next with a value
91
+ *
92
+ * @example
93
+ * ```ts
94
+ * router.beforeEach(composeGuards([
95
+ * authGuard(),
96
+ * orgAccessGuard(),
97
+ * featureFlagGuard(),
98
+ * ]))
99
+ * ```
100
+ */
101
+ export function composeGuards(guards: NavigationGuard[]): NavigationGuard {
102
+ return async (to, from, next) => {
103
+ let guardIndex = 0
104
+
105
+ const runNextGuard = async (): Promise<void> => {
106
+ if (guardIndex >= guards.length) {
107
+ // All guards passed, allow navigation
108
+ next()
109
+ return
110
+ }
111
+
112
+ const guard = guards[guardIndex]
113
+ guardIndex++
114
+
115
+ // Run the current guard
116
+ await guard(to, from, (result?: any) => {
117
+ if (result !== undefined) {
118
+ // Guard blocked or redirected, stop here
119
+ next(result)
120
+ } else {
121
+ // Guard passed, run next guard
122
+ runNextGuard()
123
+ }
124
+ })
125
+ }
126
+
127
+ await runNextGuard()
128
+ }
129
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Redirect configuration types for auth library
3
+ */
4
+
5
+ export interface RedirectConfig {
6
+ /**
7
+ * Query parameter key used to store the redirect URL
8
+ * After login, read this param to redirect users back to their intended destination
9
+ * @default 'redirect'
10
+ */
11
+ queryKey?: string
12
+
13
+ /**
14
+ * Default fallback URL when no redirect is specified
15
+ * @default '/'
16
+ */
17
+ fallback?: string
18
+
19
+ /**
20
+ * Routes that require NO authentication (login, signup, forgot password, etc)
21
+ * Authenticated users will be automatically redirected away from these pages
22
+ * @default ['Login', 'Signup', 'ForgotPassword', 'ResetPassword', 'Callback']
23
+ */
24
+ noAuthRoutes?: string[]
25
+
26
+ /**
27
+ * Route name to redirect to when authenticated users try to access auth-only pages
28
+ * @default '/'
29
+ */
30
+ authenticatedRedirect?: string
31
+
32
+ /**
33
+ * Route name to redirect to when unauthenticated users try to access protected pages
34
+ * @default 'Login'
35
+ */
36
+ loginRoute?: string
37
+
38
+ /**
39
+ * Meta key used to check if a route requires authentication
40
+ * @default 'auth'
41
+ * @example In your route: `meta: { auth: true }`
42
+ */
43
+ authMetaKey?: string
44
+
45
+ /**
46
+ * Enable automatic redirect handling after login
47
+ * When true, the library automatically redirects users after successful login
48
+ * @default true
49
+ */
50
+ autoRedirect?: boolean
51
+
52
+ /**
53
+ * Enable redirect preservation for protected routes
54
+ * When enabled, the original URL is preserved as a query param after redirecting to login
55
+ * @default true
56
+ */
57
+ preserveRedirect?: boolean
58
+
59
+ /**
60
+ * Optional allowed redirect path patterns for security validation
61
+ * If specified, only URLs matching these patterns will be allowed
62
+ * @default undefined (allows all internal paths)
63
+ */
64
+ allowedPaths?: RegExp[]
65
+ }
66
+
67
+ /**
68
+ * Normalized redirect configuration with all defaults applied
69
+ */
70
+ export interface NormalizedRedirectConfig extends Required<Omit<RedirectConfig, 'allowedPaths'>> {
71
+ allowedPaths?: RegExp[]
72
+ }
73
+
74
+ /**
75
+ * Default redirect configuration
76
+ */
77
+ export const DEFAULT_REDIRECT_CONFIG: NormalizedRedirectConfig = {
78
+ queryKey: 'redirect',
79
+ fallback: '/',
80
+ noAuthRoutes: ['Login', 'Signup', 'ForgotPassword', 'ResetPassword', 'Callback'],
81
+ authenticatedRedirect: '/',
82
+ loginRoute: 'Login',
83
+ authMetaKey: 'auth',
84
+ autoRedirect: true,
85
+ preserveRedirect: true,
86
+ }
87
+
88
+ /**
89
+ * Normalize redirect configuration by applying defaults
90
+ */
91
+ export function normalizeRedirectConfig(config?: RedirectConfig): NormalizedRedirectConfig {
92
+ return {
93
+ ...DEFAULT_REDIRECT_CONFIG,
94
+ ...config,
95
+ }
96
+ }
package/src/useAuth.ts CHANGED
@@ -11,19 +11,40 @@ import type {
11
11
  SSOCallbackRequest,
12
12
  SSOLinkRequest,
13
13
  } from './types'
14
+ import type { RedirectConfig, NormalizedRedirectConfig } from './types/redirect'
14
15
  import { ref, computed } from 'vue'
15
16
  import { AuthApi } from './api'
16
17
  import { setAuthContext, sso } from './sso'
17
18
  import { AuthState, accountToUser } from './types'
19
+ import { normalizeRedirectConfig } from './types/redirect'
18
20
  import { EventEmitter } from './utils'
19
21
 
20
22
  // Global state
21
23
  let authApi: AuthApi | null = null
22
24
  let eventEmitter: EventEmitter | null = null
25
+ let redirectConfig: NormalizedRedirectConfig | null = null
26
+ let autoRedirectRouter: any = null // Router instance for auto-redirect
27
+ let cachedAuthGuard: any = null // Cached router guard
23
28
  const accountInfo = ref<AccountInfo | null>(null)
24
29
 
25
30
  interface InitParams {
26
31
  baseURL: string
32
+ /**
33
+ * Redirect configuration for authentication flows
34
+ * @see RedirectConfig
35
+ */
36
+ redirect?: RedirectConfig
37
+ }
38
+
39
+ /**
40
+ * Get the current redirect configuration
41
+ * Used internally by router guard
42
+ */
43
+ export function getRedirectConfig(): NormalizedRedirectConfig {
44
+ if (!redirectConfig) {
45
+ throw new Error('Redirect config not initialized. Did you call createAuth with redirect config?')
46
+ }
47
+ return redirectConfig
27
48
  }
28
49
 
29
50
  // Initialize auth
@@ -36,7 +57,17 @@ export function createAuth(params: InitParams) {
36
57
  eventEmitter = new EventEmitter()
37
58
  }
38
59
 
39
- return {
60
+ // Store redirect configuration
61
+ if (params.redirect) {
62
+ redirectConfig = normalizeRedirectConfig(params.redirect)
63
+ }
64
+
65
+ // Setup auto-redirect on login if enabled
66
+ if (redirectConfig?.autoRedirect) {
67
+ setupAutoRedirect()
68
+ }
69
+
70
+ const authInstance = {
40
71
  // Event listener methods
41
72
  on<K extends AuthState>(event: K, handler: AuthEventMap[K]): void {
42
73
  if (eventEmitter) {
@@ -56,11 +87,107 @@ export function createAuth(params: InitParams) {
56
87
  }
57
88
  },
58
89
 
90
+ /**
91
+ * Connect external dependencies like Vue Router
92
+ * Automatically sets up router guard when router is provided
93
+ * @param dependency - Vue Router instance or other plugins
94
+ * @param options - Configuration options
95
+ * @param options.guard - Whether to automatically set up auth guard (default: true)
96
+ * @example
97
+ * ```ts
98
+ * // Auto setup (default)
99
+ * auth.use(router)
100
+ *
101
+ * // Manual guard control (for custom composition)
102
+ * auth.use(router, { guard: false })
103
+ * router.beforeEach(async (to, from, next) => {
104
+ * // Custom logic first
105
+ * if (!hasOrgAccess(to)) return next('/no-access')
106
+ * // Then run auth guard
107
+ * return auth.routerGuard()(to, from, next)
108
+ * })
109
+ * ```
110
+ */
111
+ use(dependency: any, options: { guard?: boolean } = {}) {
112
+ const { guard = true } = options
113
+
114
+ // Detect if it's a router by checking for common router properties
115
+ if (dependency && (dependency.beforeEach || dependency.push || dependency.currentRoute)) {
116
+ autoRedirectRouter = dependency
117
+ // Automatically set up the auth guard unless disabled
118
+ if (guard) {
119
+ dependency.beforeEach(authInstance.routerGuard())
120
+ }
121
+ }
122
+ return authInstance
123
+ },
124
+
125
+ /**
126
+ * Create a Vue Router navigation guard for authentication
127
+ * Protects routes requiring authentication and handles redirect logic
128
+ * Note: Automatically called by auth.use(router), only use directly for custom setups
129
+ * @example
130
+ * ```ts
131
+ * // Automatic (recommended)
132
+ * auth.use(router)
133
+ *
134
+ * // Manual (for custom setups)
135
+ * router.beforeEach(auth.routerGuard())
136
+ * ```
137
+ */
138
+ routerGuard() {
139
+ // Return factory that lazily loads authGuard to avoid circular dependency
140
+ if (cachedAuthGuard === null) {
141
+ cachedAuthGuard = async (to: any, from: any, next: any) => {
142
+ const { authGuard } = await import('./router')
143
+ const guard = authGuard()
144
+ // Cache the actual guard for next time
145
+ cachedAuthGuard = guard
146
+ return guard(to, from, next)
147
+ }
148
+ }
149
+ return cachedAuthGuard
150
+ },
151
+
152
+ /**
153
+ * Vue plugin install method
154
+ * Makes auth available globally as $auth
155
+ * @example
156
+ * ```ts
157
+ * app.use(auth)
158
+ * ```
159
+ */
59
160
  install(app: App) {
60
161
  // Make auth available globally
61
162
  app.config.globalProperties.$auth = useAuth()
62
163
  },
63
164
  }
165
+
166
+ return authInstance
167
+ }
168
+
169
+ /**
170
+ * Setup automatic redirect after successful login
171
+ */
172
+ function setupAutoRedirect() {
173
+ if (!eventEmitter || !redirectConfig) return
174
+
175
+ eventEmitter.on(AuthState.LOGIN, async () => {
176
+ // Only auto-redirect if router is available
177
+ if (!autoRedirectRouter) {
178
+ console.warn('[Auth] Auto-redirect enabled but router not set. Call setAuthRouter(router) in your app setup.')
179
+ return
180
+ }
181
+
182
+ // Dynamic import to avoid circular dependency
183
+ const { performRedirect } = await import('./redirect')
184
+
185
+ try {
186
+ await performRedirect(autoRedirectRouter, redirectConfig!)
187
+ } catch (error) {
188
+ console.error('[Auth] Auto-redirect error:', error)
189
+ }
190
+ })
64
191
  }
65
192
 
66
193
  // Composable