@jasperoosthoek/zustand-auth-registry 0.0.1 → 0.0.3

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/authStore.ts CHANGED
@@ -2,43 +2,72 @@ import { create, StoreApi, UseBoundStore } from 'zustand';
2
2
  import { ValidatedAuthConfig, TokenData } from './authConfig';
3
3
 
4
4
  export type AuthState<U> = {
5
- isAuthenticated: boolean;
5
+ isAuthenticated: boolean | null; // null = not checked yet (cookie mode)
6
6
  user: U | null;
7
- tokens: TokenData | null;
8
-
9
- // OAuth 2.0 methods
7
+ tokens: TokenData | null; // null in cookie mode or when logged out
8
+
9
+ // Methods
10
10
  setTokens: (tokens: TokenData) => void;
11
+ setBearerToken: (token: string) => void; // Convenience for simple Bearer token auth
12
+ setAuthenticated: (authenticated: boolean) => void; // For cookie mode
11
13
  setUser: (user: U) => void;
12
14
  unsetUser: () => void;
13
15
  isTokenExpired: () => boolean;
14
-
15
- // Backward compatibility
16
- token: string;
17
- setToken: (token: string) => void;
18
16
  };
19
17
 
20
18
  export type AuthStore<U> = UseBoundStore<StoreApi<AuthState<U>>> & {
21
19
  config: ValidatedAuthConfig<U>;
22
20
  };
23
21
 
22
+ // Methods that modify state (require CSRF protection)
23
+ const CSRF_METHODS = ['post', 'put', 'patch', 'delete'];
24
+
25
+ const setupCsrfInterceptor = <U>(config: ValidatedAuthConfig<U>): number | null => {
26
+ if (!config.cookieAuth?.csrf.enabled) {
27
+ return null;
28
+ }
29
+
30
+ const { headerName, getToken } = config.cookieAuth.csrf;
31
+
32
+ return config.axios.interceptors.request.use((requestConfig) => {
33
+ const method = requestConfig.method?.toLowerCase();
34
+ if (method && CSRF_METHODS.includes(method)) {
35
+ const csrfToken = getToken();
36
+ if (csrfToken) {
37
+ requestConfig.headers[headerName] = csrfToken;
38
+ }
39
+ }
40
+ return requestConfig;
41
+ });
42
+ };
43
+
24
44
  export const createAuthStore = <U>(config: ValidatedAuthConfig<U>): AuthStore<U> => {
25
- const { persistence } = config;
26
-
45
+ const { persistence, cookieAuth } = config;
46
+
47
+ // Set up CSRF interceptor if enabled
48
+ setupCsrfInterceptor(config);
49
+
27
50
  const getStoredTokens = (): TokenData | null => {
51
+ // Cookie mode: No client-side tokens
52
+ if (cookieAuth?.enabled) {
53
+ return null;
54
+ }
55
+
56
+ // Token mode: Read from storage
28
57
  if (!persistence.enabled) return null;
29
58
  try {
30
59
  const accessToken = persistence.storage.getItem(persistence.tokenKey);
31
60
  if (!accessToken) return null;
32
-
61
+
33
62
  const refreshToken = persistence.storage.getItem(persistence.refreshTokenKey);
34
63
  const expiryString = persistence.storage.getItem(persistence.expiryKey);
35
64
  const expiresAt = expiryString ? parseInt(expiryString, 10) : undefined;
36
-
65
+
37
66
  return {
38
67
  accessToken,
39
68
  refreshToken: refreshToken || undefined,
40
69
  expiresAt: expiresAt && !isNaN(expiresAt) ? expiresAt : undefined,
41
- tokenType: 'Bearer', // Default, will be updated on setTokens
70
+ tokenType: 'Bearer',
42
71
  };
43
72
  } catch {
44
73
  return null;
@@ -57,30 +86,37 @@ export const createAuthStore = <U>(config: ValidatedAuthConfig<U>): AuthStore<U>
57
86
 
58
87
  const initialTokens = getStoredTokens();
59
88
  const initialUser = getStoredUser();
60
- const initialIsAuthenticated = !!initialTokens?.accessToken;
89
+
90
+ // Cookie mode: null (unknown until checkAuth)
91
+ // Token mode: true/false based on token presence
92
+ const initialIsAuthenticated = cookieAuth?.enabled
93
+ ? null
94
+ : !!initialTokens?.accessToken;
61
95
 
62
96
  const store = create<AuthState<U>>((set, get) => ({
63
97
  tokens: initialTokens,
64
98
  user: initialUser,
65
99
  isAuthenticated: initialIsAuthenticated,
66
100
 
67
- // OAuth 2.0 methods
68
101
  setTokens: (tokens: TokenData) => {
69
- const user = get().user;
70
- const isAuthenticated = !!tokens.accessToken;
71
-
72
- set({ tokens, isAuthenticated, token: tokens.accessToken });
73
-
102
+ set({ tokens, isAuthenticated: true });
103
+
104
+ // Cookie mode: No localStorage persistence for tokens
105
+ if (cookieAuth?.enabled) {
106
+ return;
107
+ }
108
+
109
+ // Token mode: Persist to storage
74
110
  if (persistence.enabled) {
75
111
  try {
76
112
  persistence.storage.setItem(persistence.tokenKey, tokens.accessToken);
77
-
113
+
78
114
  if (tokens.refreshToken) {
79
115
  persistence.storage.setItem(persistence.refreshTokenKey, tokens.refreshToken);
80
116
  } else {
81
117
  persistence.storage.removeItem(persistence.refreshTokenKey);
82
118
  }
83
-
119
+
84
120
  if (tokens.expiresAt) {
85
121
  persistence.storage.setItem(persistence.expiryKey, tokens.expiresAt.toString());
86
122
  } else {
@@ -92,12 +128,17 @@ export const createAuthStore = <U>(config: ValidatedAuthConfig<U>): AuthStore<U>
92
128
  }
93
129
  },
94
130
 
131
+ setBearerToken: (token: string) => {
132
+ get().setTokens({ accessToken: token, tokenType: 'Bearer' });
133
+ },
134
+
135
+ setAuthenticated: (authenticated: boolean) => {
136
+ set({ isAuthenticated: authenticated });
137
+ },
138
+
95
139
  setUser: (user: U) => {
96
- const tokens = get().tokens;
97
- const isAuthenticated = !!tokens?.accessToken;
98
-
99
- set({ user, isAuthenticated });
100
-
140
+ set({ user });
141
+
101
142
  if (persistence.enabled) {
102
143
  try {
103
144
  persistence.storage.setItem(persistence.userKey, JSON.stringify(user));
@@ -108,8 +149,8 @@ export const createAuthStore = <U>(config: ValidatedAuthConfig<U>): AuthStore<U>
108
149
  },
109
150
 
110
151
  unsetUser: () => {
111
- set({ user: null, tokens: null, isAuthenticated: false, token: '' });
112
-
152
+ set({ user: null, tokens: null, isAuthenticated: false });
153
+
113
154
  if (persistence.enabled) {
114
155
  try {
115
156
  persistence.storage.removeItem(persistence.tokenKey);
@@ -124,36 +165,9 @@ export const createAuthStore = <U>(config: ValidatedAuthConfig<U>): AuthStore<U>
124
165
 
125
166
  isTokenExpired: () => {
126
167
  const tokens = get().tokens;
127
- if (!tokens?.expiresAt) return false; // No expiry info means no expiration
168
+ if (!tokens?.expiresAt) return false;
128
169
  return Date.now() >= tokens.expiresAt;
129
170
  },
130
-
131
- // Backward compatibility - computed property
132
- token: initialTokens?.accessToken || '',
133
-
134
- setToken: (token: string) => {
135
- const currentTokens = get().tokens;
136
- const user = get().user;
137
- const newTokens: TokenData = {
138
- accessToken: token,
139
- refreshToken: currentTokens?.refreshToken,
140
- expiresAt: currentTokens?.expiresAt,
141
- tokenType: currentTokens?.tokenType || 'Bearer', // Standard default
142
- scope: currentTokens?.scope,
143
- };
144
-
145
- const isAuthenticated = !!token;
146
- set({ tokens: newTokens, token, isAuthenticated });
147
-
148
- // Handle persistence
149
- if (persistence.enabled) {
150
- try {
151
- persistence.storage.setItem(persistence.tokenKey, token);
152
- } catch (error) {
153
- config.onError?.(error);
154
- }
155
- }
156
- },
157
171
  }));
158
172
 
159
173
  return Object.assign(store, { config });
@@ -19,4 +19,4 @@ export function createAuthRegistry<AuthModels extends Record<string, any>>() {
19
19
  }
20
20
 
21
21
  return getAuthStore;
22
- }
22
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Authentication error codes
3
+ */
4
+ export enum AuthErrorCode {
5
+ INVALID_CREDENTIALS = 'INVALID_CREDENTIALS',
6
+ TOKEN_EXPIRED = 'TOKEN_EXPIRED',
7
+ TOKEN_INVALID = 'TOKEN_INVALID',
8
+ REFRESH_FAILED = 'REFRESH_FAILED',
9
+ NETWORK_ERROR = 'NETWORK_ERROR',
10
+ USER_NOT_FOUND = 'USER_NOT_FOUND',
11
+ UNAUTHORIZED = 'UNAUTHORIZED',
12
+ CSRF_TOKEN_MISSING = 'CSRF_TOKEN_MISSING',
13
+ FORBIDDEN = 'FORBIDDEN',
14
+ UNKNOWN = 'UNKNOWN',
15
+ }
16
+
17
+ /**
18
+ * Typed authentication error
19
+ */
20
+ export class AuthError extends Error {
21
+ constructor(
22
+ public code: AuthErrorCode,
23
+ public originalError?: any,
24
+ message?: string
25
+ ) {
26
+ super(message || code);
27
+ this.name = 'AuthError';
28
+
29
+ // Maintain proper stack trace for where our error was thrown (only available on V8)
30
+ if (Error.captureStackTrace) {
31
+ Error.captureStackTrace(this, AuthError);
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Convert error to JSON (excludes originalError in production)
37
+ */
38
+ toJSON() {
39
+ return {
40
+ code: this.code,
41
+ message: this.message,
42
+ name: this.name,
43
+ };
44
+ }
45
+
46
+ /**
47
+ * Check if error is an AuthError
48
+ */
49
+ static isAuthError(error: any): error is AuthError {
50
+ return error instanceof AuthError;
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Create typed AuthError from any error
56
+ */
57
+ export function createAuthError(error: any): AuthError {
58
+ if (AuthError.isAuthError(error)) {
59
+ return error;
60
+ }
61
+
62
+ // Axios error
63
+ if (error.response) {
64
+ const status = error.response.status;
65
+ const data = error.response.data;
66
+
67
+ switch (status) {
68
+ case 401:
69
+ // Check if it's an expired token
70
+ if (data?.detail?.toLowerCase().includes('expired') ||
71
+ data?.message?.toLowerCase().includes('expired')) {
72
+ return new AuthError(AuthErrorCode.TOKEN_EXPIRED, error, 'Token has expired');
73
+ }
74
+ if (data?.detail?.toLowerCase().includes('invalid') ||
75
+ data?.detail?.toLowerCase().includes('credentials')) {
76
+ return new AuthError(AuthErrorCode.INVALID_CREDENTIALS, error, 'Invalid credentials');
77
+ }
78
+ return new AuthError(AuthErrorCode.UNAUTHORIZED, error, 'Unauthorized');
79
+
80
+ case 403:
81
+ if (data?.detail?.toLowerCase().includes('csrf')) {
82
+ return new AuthError(AuthErrorCode.CSRF_TOKEN_MISSING, error, 'CSRF token missing or invalid');
83
+ }
84
+ return new AuthError(AuthErrorCode.FORBIDDEN, error, 'Access forbidden');
85
+
86
+ case 404:
87
+ if (data?.detail?.toLowerCase().includes('user')) {
88
+ return new AuthError(AuthErrorCode.USER_NOT_FOUND, error, 'User not found');
89
+ }
90
+ return new AuthError(AuthErrorCode.UNKNOWN, error, 'Resource not found');
91
+
92
+ default:
93
+ return new AuthError(AuthErrorCode.UNKNOWN, error, `HTTP ${status} error`);
94
+ }
95
+ }
96
+
97
+ // Network error (no response received)
98
+ if (error.request) {
99
+ return new AuthError(AuthErrorCode.NETWORK_ERROR, error, 'Network error - no response received');
100
+ }
101
+
102
+ // Other errors
103
+ return new AuthError(AuthErrorCode.UNKNOWN, error, error.message || 'Unknown error');
104
+ }
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './authConfig';
2
2
  export * from './authStore';
3
3
  export * from './createAuthRegistry';
4
- export * from './useAuth';
4
+ export * from './useAuth';
5
+ export * from './errors';
package/src/useAuth.ts CHANGED
@@ -1,151 +1,167 @@
1
1
  import { useEffect, useCallback } from 'react';
2
- import axios from 'axios';
3
2
  import { AuthStore } from './authStore';
4
- import { TokenData } from './authConfig';
3
+
4
+ // Promise deduplication: prevents multiple concurrent checkAuth calls per store
5
+ const pendingCheckAuth = new WeakMap<AuthStore<any>, Promise<boolean>>();
5
6
 
6
7
  export function useAuth<U>(store: AuthStore<U>) {
7
- const { setTokens, setUser, unsetUser, tokens, user, isTokenExpired } = store();
8
+ const { setTokens, setAuthenticated, setUser, unsetUser, tokens, user, isAuthenticated, isTokenExpired } = store();
8
9
  const config = store.config;
9
10
 
10
- // Backward compatibility
11
- const { setToken, token } = store();
11
+ // Set axios Authorization header (token mode only)
12
+ // Note: CSRF is handled by interceptor in authStore.ts
13
+ const setAxiosAuth = useCallback((token?: string, tokenType?: string) => {
14
+ if (config.cookieAuth?.enabled) {
15
+ // Cookie mode: CSRF handled by interceptor, no Authorization header needed
16
+ return;
17
+ }
12
18
 
13
- // OAuth 2.0 refresh token functionality
14
- const refreshTokens = useCallback(async (): Promise<boolean> => {
15
- if (!tokens?.refreshToken) {
16
- return false;
19
+ // Token mode: Set Authorization header
20
+ if (token) {
21
+ config.axios.defaults.headers.common['Authorization'] = config.formatAuthHeader(token, tokenType);
22
+ } else {
23
+ delete config.axios.defaults.headers.common['Authorization'];
17
24
  }
25
+ }, [config]);
26
+
27
+ // Refresh tokens
28
+ const refresh = useCallback(async (): Promise<boolean> => {
29
+ if (!config.refreshUrl) return false;
18
30
 
19
31
  try {
20
- const response = await config.axios.post(config.tokenUrl, {
21
- grant_type: 'refresh_token',
32
+ // Cookie mode: Just call refresh endpoint, server handles cookie
33
+ if (config.cookieAuth?.enabled) {
34
+ const headers = getCsrfHeaders(config);
35
+ await config.axios.post(config.refreshUrl, {}, { headers });
36
+ return true;
37
+ }
38
+
39
+ // Token mode: Send refresh token, get new tokens
40
+ if (!tokens?.refreshToken) return false;
41
+
42
+ const response = await config.axios.post(config.refreshUrl, {
22
43
  refresh_token: tokens.refreshToken,
23
44
  });
24
45
 
25
46
  const newTokens = config.extractTokens(response.data);
26
47
  setTokens(newTokens);
27
48
  setAxiosAuth(newTokens.accessToken, newTokens.tokenType);
28
-
29
- config.onTokenRefresh?.(newTokens);
30
49
  return true;
31
50
  } catch (error) {
32
- // Refresh failed, clear tokens
33
51
  unsetUser();
34
52
  setAxiosAuth();
35
53
  config.onError?.(error);
36
54
  return false;
37
55
  }
38
- }, [tokens?.refreshToken, config, setTokens, unsetUser]);
56
+ }, [tokens, config, setTokens, unsetUser, setAxiosAuth]);
39
57
 
40
- // Auto-refresh and authentication setup
41
- useEffect(() => {
42
- // Handle current tokens
43
- if (tokens?.accessToken) {
44
- // Set axios headers immediately
45
- setAxiosAuth(tokens.accessToken, tokens.tokenType);
58
+ // Check authentication (cookie mode) - with promise deduplication
59
+ const checkAuth = useCallback(async (): Promise<boolean> => {
60
+ const authCheckUrl = config.authCheckUrl;
61
+ if (!config.cookieAuth?.enabled || !authCheckUrl) {
62
+ return false;
63
+ }
46
64
 
47
- // Check if token is expired or about to expire
48
- if (isTokenExpired()) {
49
- // Token is already expired, try to refresh
50
- if (tokens.refreshToken && config.autoRefresh) {
51
- refreshTokens();
52
- } else {
53
- unsetUser();
54
- }
55
- return;
56
- }
65
+ // Return existing promise if check is already in progress
66
+ const pending = pendingCheckAuth.get(store);
67
+ if (pending) {
68
+ return pending;
69
+ }
57
70
 
58
- // Set up auto-refresh timer if we have expiry info
59
- if (tokens.expiresAt && tokens.refreshToken && config.autoRefresh) {
60
- const timeUntilExpiry = tokens.expiresAt - Date.now();
61
- const refreshTime = Math.max(timeUntilExpiry - config.refreshThreshold, 0);
71
+ const doCheck = async (): Promise<boolean> => {
72
+ try {
73
+ const headers = getCsrfHeaders(config);
74
+ const response = await config.axios.get(authCheckUrl, { headers });
62
75
 
63
- const timer = setTimeout(() => {
64
- refreshTokens();
65
- }, refreshTime);
76
+ if (response.data.authenticated) {
77
+ setAuthenticated(true);
66
78
 
67
- return () => clearTimeout(timer);
68
- }
79
+ const extractedUser = config.extractUser?.(response.data);
80
+ if (extractedUser) {
81
+ setUser(extractedUser);
82
+ } else if (config.getUserUrl) {
83
+ await getCurrentUser();
84
+ }
69
85
 
70
- // Try to get user info if missing
71
- try {
72
- if (!user && (config.userInfoUrl || config.getUserUrl)) {
73
- getCurrentUser();
86
+ return true;
74
87
  }
88
+
89
+ setAuthenticated(false);
90
+ return false;
75
91
  } catch (error) {
76
- if (axios.isAxiosError(error) && error.response?.status === 403) {
77
- unsetUser();
78
- setAxiosAuth();
79
- }
92
+ setAuthenticated(false);
80
93
  config.onError?.(error);
94
+ return false;
95
+ } finally {
96
+ pendingCheckAuth.delete(store);
81
97
  }
82
- }
83
- }, [tokens, user, config.autoRefresh, config.refreshThreshold, config.userInfoUrl, config.getUserUrl, isTokenExpired, refreshTokens, unsetUser]);
98
+ };
84
99
 
85
- const setAxiosAuth = (token?: string, tokenType?: string) => {
86
- if (typeof token !== 'undefined' && token) {
87
- config.axios.defaults.headers.common['Authorization'] = config.formatAuthHeader(token, tokenType);
88
- } else {
89
- delete config.axios.defaults.headers.common['Authorization'];
90
- }
91
- };
100
+ const promise = doCheck();
101
+ pendingCheckAuth.set(store, promise);
102
+ return promise;
103
+ }, [store, config, setAuthenticated, setUser]);
104
+
105
+ // Get current user
106
+ const getCurrentUser = useCallback(async () => {
107
+ if (!config.getUserUrl) return;
92
108
 
93
- const login = async (
94
- credentials: Record<string, string>,
95
- callback?: () => void
96
- ) => {
97
109
  try {
98
- const res = await config.axios.post(config.tokenUrl, credentials);
99
- const tokens = config.extractTokens(res.data);
100
- setTokens(tokens);
101
- setAxiosAuth(tokens.accessToken, tokens.tokenType);
102
-
103
- if (config.userInfoUrl || config.getUserUrl) {
104
- await getCurrentUser();
105
- }
106
-
107
- if (user) {
108
- config.onLogin?.(user);
109
- }
110
- callback?.();
111
- } catch (err) {
110
+ const res = await config.axios.get<U>(config.getUserUrl);
111
+ setUser(res.data);
112
+ } catch (error) {
112
113
  unsetUser();
113
114
  setAxiosAuth();
114
- config.onError?.(err);
115
+ config.onError?.(error);
116
+ throw error;
115
117
  }
116
- };
118
+ }, [config, setUser, unsetUser, setAxiosAuth]);
117
119
 
118
- const getCurrentUser = async () => {
119
- const userUrl = config.userInfoUrl || config.getUserUrl;
120
- if (!userUrl) return;
120
+ // Login
121
+ const login = async (credentials: Record<string, string>, callback?: () => void) => {
121
122
  try {
122
- const res = await config.axios.get<U>(userUrl);
123
- setUser(res.data);
124
- } catch (err) {
123
+ const headers = config.cookieAuth?.enabled ? getCsrfHeaders(config) : {};
124
+ const res = await config.axios.post(config.loginUrl, credentials, { headers });
125
+
126
+ if (config.cookieAuth?.enabled) {
127
+ // Cookie mode: Server sets httpOnly cookie, just mark as authenticated
128
+ setAuthenticated(true);
129
+ } else {
130
+ // Token mode: Extract and store tokens
131
+ const newTokens = config.extractTokens(res.data);
132
+ setTokens(newTokens);
133
+ setAxiosAuth(newTokens.accessToken, newTokens.tokenType);
134
+ }
135
+
136
+ // Extract user from response
137
+ const extractedUser = config.extractUser?.(res.data);
138
+ if (extractedUser) {
139
+ setUser(extractedUser);
140
+ config.onLogin?.(extractedUser);
141
+ } else if (config.getUserUrl) {
142
+ await getCurrentUser();
143
+ const currentUser = store.getState().user;
144
+ if (currentUser) config.onLogin?.(currentUser);
145
+ }
146
+
147
+ callback?.();
148
+ } catch (error) {
125
149
  unsetUser();
126
150
  setAxiosAuth();
127
- config.onError?.(err);
151
+ config.onError?.(error);
152
+ throw error;
128
153
  }
129
154
  };
130
155
 
156
+ // Logout
131
157
  const logout = async () => {
132
158
  try {
133
- // If we have a revoke/logout URL, call it
134
- const logoutUrl = config.revokeUrl || config.logoutUrl;
135
- if (logoutUrl) {
136
- if (config.revokeUrl && tokens?.refreshToken) {
137
- // OAuth revoke
138
- await config.axios.post(logoutUrl, {
139
- token: tokens.refreshToken,
140
- token_type_hint: 'refresh_token'
141
- });
142
- } else {
143
- // Simple logout
144
- await config.axios.post(logoutUrl);
145
- }
159
+ if (config.logoutUrl) {
160
+ const headers = config.cookieAuth?.enabled ? getCsrfHeaders(config) : {};
161
+ await config.axios.post(config.logoutUrl, {}, { headers });
146
162
  }
147
- } catch (err) {
148
- config.onError?.(err);
163
+ } catch (error) {
164
+ config.onError?.(error);
149
165
  } finally {
150
166
  unsetUser();
151
167
  setAxiosAuth();
@@ -153,5 +169,57 @@ export function useAuth<U>(store: AuthStore<U>) {
153
169
  }
154
170
  };
155
171
 
156
- return { login, getCurrentUser, logout, refreshTokens };
157
- }
172
+ // Auto-setup on mount
173
+ useEffect(() => {
174
+ // Cookie mode: Check authentication if not yet determined
175
+ if (config.cookieAuth?.enabled) {
176
+ if (isAuthenticated === null) {
177
+ checkAuth();
178
+ }
179
+ return;
180
+ }
181
+
182
+ // Token mode: Setup headers and auto-refresh
183
+ if (tokens?.accessToken) {
184
+ setAxiosAuth(tokens.accessToken, tokens.tokenType);
185
+
186
+ // Check if expired
187
+ if (isTokenExpired()) {
188
+ if (tokens.refreshToken && config.autoRefresh) {
189
+ refresh();
190
+ } else {
191
+ unsetUser();
192
+ }
193
+ return;
194
+ }
195
+
196
+ // Setup auto-refresh timer
197
+ if (tokens.expiresAt && tokens.refreshToken && config.autoRefresh) {
198
+ const timeUntilExpiry = tokens.expiresAt - Date.now();
199
+ const refreshTime = Math.max(timeUntilExpiry - config.refreshThreshold, 0);
200
+
201
+ const timer = setTimeout(refresh, refreshTime);
202
+ return () => clearTimeout(timer);
203
+ }
204
+
205
+ // Fetch user if missing
206
+ if (!user && config.getUserUrl) {
207
+ getCurrentUser().catch(() => {});
208
+ }
209
+ }
210
+ }, [tokens, user, isAuthenticated, config, isTokenExpired, refresh, checkAuth, getCurrentUser, setAxiosAuth, unsetUser]);
211
+
212
+ return { login, logout, refresh, checkAuth, getCurrentUser };
213
+ }
214
+
215
+ // Helper: Get CSRF headers if enabled (uses getToken from config)
216
+ function getCsrfHeaders(config: any): Record<string, string> {
217
+ const headers: Record<string, string> = {};
218
+ if (config.cookieAuth?.csrf?.enabled) {
219
+ const csrfToken = config.cookieAuth.csrf.getToken();
220
+ if (csrfToken) {
221
+ headers[config.cookieAuth.csrf.headerName] = csrfToken;
222
+ }
223
+ }
224
+ return headers;
225
+ }
@@ -1 +0,0 @@
1
- import '@testing-library/react';