@spidy092/auth-client 1.0.18 → 2.0.0
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/api.js +59 -16
- package/core.js +54 -19
- package/package.json +1 -1
- package/token.js +348 -90
package/api.js
CHANGED
|
@@ -1,50 +1,93 @@
|
|
|
1
1
|
// auth-client/api.js
|
|
2
2
|
import axios from 'axios';
|
|
3
|
-
import { getToken } from './token';
|
|
4
3
|
import { getConfig } from './config';
|
|
4
|
+
import { getToken, setToken, clearToken } from './token';
|
|
5
|
+
import { refreshToken as performRefresh } from './core';
|
|
5
6
|
|
|
6
|
-
// ✅ Fixed: Create instance without baseURL initially
|
|
7
7
|
const api = axios.create({
|
|
8
8
|
withCredentials: true,
|
|
9
9
|
});
|
|
10
10
|
|
|
11
|
-
// ✅ Fixed: Set baseURL dynamically in interceptor
|
|
12
11
|
api.interceptors.request.use((config) => {
|
|
13
|
-
|
|
12
|
+
const runtimeConfig = getConfig();
|
|
13
|
+
|
|
14
14
|
if (!config.baseURL) {
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
config.baseURL = runtimeConfig?.authBaseUrl || 'http://localhost:4000/auth';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (!config.headers) {
|
|
19
|
+
config.headers = {};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (runtimeConfig?.clientKey && !config.headers['X-Client-Key']) {
|
|
23
|
+
config.headers['X-Client-Key'] = runtimeConfig.clientKey;
|
|
17
24
|
}
|
|
18
|
-
|
|
25
|
+
|
|
19
26
|
const token = getToken();
|
|
20
27
|
if (token) {
|
|
21
28
|
config.headers.Authorization = `Bearer ${token}`;
|
|
22
29
|
}
|
|
30
|
+
|
|
23
31
|
return config;
|
|
24
32
|
});
|
|
25
33
|
|
|
26
|
-
|
|
34
|
+
let refreshPromise = null;
|
|
35
|
+
|
|
27
36
|
api.interceptors.response.use(
|
|
28
37
|
(response) => response,
|
|
29
|
-
(error) => {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
38
|
+
async (error) => {
|
|
39
|
+
const { response, config } = error || {};
|
|
40
|
+
|
|
41
|
+
if (!response || !config) {
|
|
42
|
+
return Promise.reject(error);
|
|
33
43
|
}
|
|
44
|
+
|
|
45
|
+
if (response.status !== 401 || config._retry) {
|
|
46
|
+
return Promise.reject(error);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
config._retry = true;
|
|
50
|
+
|
|
51
|
+
if (!refreshPromise) {
|
|
52
|
+
refreshPromise = performRefresh()
|
|
53
|
+
.then((newToken) => {
|
|
54
|
+
refreshPromise = null;
|
|
55
|
+
if (newToken) {
|
|
56
|
+
setToken(newToken);
|
|
57
|
+
}
|
|
58
|
+
return newToken;
|
|
59
|
+
})
|
|
60
|
+
.catch((refreshError) => {
|
|
61
|
+
refreshPromise = null;
|
|
62
|
+
clearToken();
|
|
63
|
+
throw refreshError;
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const refreshedToken = await refreshPromise;
|
|
69
|
+
|
|
70
|
+
if (refreshedToken) {
|
|
71
|
+
config.headers.Authorization = `Bearer ${refreshedToken}`;
|
|
72
|
+
return api(config);
|
|
73
|
+
}
|
|
74
|
+
} catch (refreshErr) {
|
|
75
|
+
return Promise.reject(refreshErr);
|
|
76
|
+
}
|
|
77
|
+
|
|
34
78
|
return Promise.reject(error);
|
|
35
79
|
}
|
|
36
80
|
);
|
|
37
81
|
|
|
38
|
-
|
|
39
82
|
api.validateSession = async () => {
|
|
40
83
|
try {
|
|
41
84
|
const response = await api.get('/account/validate-session');
|
|
42
85
|
return response.data.valid;
|
|
43
|
-
} catch (
|
|
44
|
-
if (
|
|
86
|
+
} catch (err) {
|
|
87
|
+
if (err.response?.status === 401) {
|
|
45
88
|
return false;
|
|
46
89
|
}
|
|
47
|
-
throw
|
|
90
|
+
throw err;
|
|
48
91
|
}
|
|
49
92
|
};
|
|
50
93
|
|
package/core.js
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
// auth-client/core.js
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
setToken,
|
|
4
|
+
clearToken,
|
|
5
|
+
getToken,
|
|
6
|
+
setRefreshToken,
|
|
7
|
+
getRefreshToken,
|
|
8
|
+
clearRefreshToken,
|
|
9
|
+
} from './token';
|
|
3
10
|
import { getConfig, isRouterMode } from './config';
|
|
4
11
|
|
|
5
12
|
// ✅ Track callback state with listeners
|
|
@@ -86,7 +93,9 @@ export function logout() {
|
|
|
86
93
|
|
|
87
94
|
// Clear local storage immediately (this will trigger listeners)
|
|
88
95
|
clearToken();
|
|
89
|
-
|
|
96
|
+
clearRefreshToken();
|
|
97
|
+
sessionStorage.removeItem('originalApp');
|
|
98
|
+
sessionStorage.removeItem('returnUrl');
|
|
90
99
|
|
|
91
100
|
if (isRouterMode()) {
|
|
92
101
|
return routerLogout(clientKey, authBaseUrl, accountUiUrl, token);
|
|
@@ -99,8 +108,8 @@ export function logout() {
|
|
|
99
108
|
async function routerLogout(clientKey, authBaseUrl, accountUiUrl, token) {
|
|
100
109
|
console.log('🏭 Enhanced Router Logout with sessionStorage');
|
|
101
110
|
|
|
102
|
-
const refreshToken =
|
|
103
|
-
console.log('Refresh token
|
|
111
|
+
const refreshToken = getRefreshToken();
|
|
112
|
+
console.log('Refresh token available:', refreshToken ? 'FOUND' : 'MISSING');
|
|
104
113
|
|
|
105
114
|
try {
|
|
106
115
|
const response = await fetch(`${authBaseUrl}/logout/${clientKey}`, {
|
|
@@ -119,7 +128,7 @@ async function routerLogout(clientKey, authBaseUrl, accountUiUrl, token) {
|
|
|
119
128
|
console.log('✅ Logout response:', data);
|
|
120
129
|
|
|
121
130
|
// Clear stored tokens
|
|
122
|
-
|
|
131
|
+
clearRefreshToken();
|
|
123
132
|
clearToken();
|
|
124
133
|
|
|
125
134
|
// Delay before redirect
|
|
@@ -132,7 +141,7 @@ async function routerLogout(clientKey, authBaseUrl, accountUiUrl, token) {
|
|
|
132
141
|
|
|
133
142
|
} catch (error) {
|
|
134
143
|
console.warn('⚠️ Logout failed:', error);
|
|
135
|
-
|
|
144
|
+
clearRefreshToken();
|
|
136
145
|
clearToken();
|
|
137
146
|
}
|
|
138
147
|
|
|
@@ -177,11 +186,11 @@ export function handleCallback() {
|
|
|
177
186
|
|
|
178
187
|
if (accessToken) {
|
|
179
188
|
setToken(accessToken);
|
|
180
|
-
|
|
181
|
-
//
|
|
189
|
+
|
|
190
|
+
// Store refresh token for future refresh calls
|
|
182
191
|
if (refreshToken) {
|
|
183
|
-
|
|
184
|
-
console.log('✅ Refresh token
|
|
192
|
+
setRefreshToken(refreshToken);
|
|
193
|
+
console.log('✅ Refresh token persisted');
|
|
185
194
|
}
|
|
186
195
|
|
|
187
196
|
// Clean URL parameters
|
|
@@ -205,37 +214,63 @@ export function resetCallbackState() {
|
|
|
205
214
|
console.log('🔄 Callback state reset');
|
|
206
215
|
}
|
|
207
216
|
|
|
217
|
+
// auth-client/core.js
|
|
208
218
|
export async function refreshToken() {
|
|
209
219
|
const { clientKey, authBaseUrl } = getConfig();
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
220
|
+
const refreshTokenValue = getRefreshToken(); // ✅ Now checks both cookie & localStorage
|
|
221
|
+
|
|
222
|
+
console.log('🔄 Refreshing token:', {
|
|
223
|
+
clientKey,
|
|
224
|
+
mode: isRouterMode() ? 'ROUTER' : 'CLIENT',
|
|
225
|
+
hasRefreshToken: !!refreshTokenValue
|
|
214
226
|
});
|
|
215
|
-
|
|
227
|
+
|
|
228
|
+
if (!refreshTokenValue) {
|
|
229
|
+
console.warn('⚠️ No refresh token available for refresh');
|
|
230
|
+
clearToken();
|
|
231
|
+
throw new Error('No refresh token available');
|
|
232
|
+
}
|
|
233
|
+
|
|
216
234
|
try {
|
|
217
235
|
const response = await fetch(`${authBaseUrl}/refresh/${clientKey}`, {
|
|
218
236
|
method: 'POST',
|
|
219
|
-
credentials: 'include',
|
|
237
|
+
credentials: 'include', // ✅ Sends cookie if available
|
|
238
|
+
headers: {
|
|
239
|
+
'Content-Type': 'application/json',
|
|
240
|
+
'X-Refresh-Token': refreshTokenValue // ✅ Also send in header as fallback
|
|
241
|
+
},
|
|
242
|
+
body: JSON.stringify({
|
|
243
|
+
refreshToken: refreshTokenValue // ✅ Also send in body
|
|
244
|
+
})
|
|
220
245
|
});
|
|
221
246
|
|
|
222
247
|
if (!response.ok) {
|
|
223
248
|
throw new Error('Refresh failed');
|
|
224
249
|
}
|
|
225
250
|
|
|
226
|
-
const { access_token } = await response.json();
|
|
227
|
-
|
|
251
|
+
const { access_token, refresh_token: new_refresh_token } = await response.json();
|
|
252
|
+
|
|
253
|
+
// ✅ Update access token (triggers listeners)
|
|
228
254
|
setToken(access_token);
|
|
255
|
+
|
|
256
|
+
// ✅ Update refresh token in BOTH storages if backend returned new one
|
|
257
|
+
if (new_refresh_token) {
|
|
258
|
+
setRefreshToken(new_refresh_token);
|
|
259
|
+
}
|
|
260
|
+
|
|
229
261
|
console.log('✅ Token refresh successful, listeners notified');
|
|
230
262
|
return access_token;
|
|
231
263
|
} catch (err) {
|
|
232
|
-
|
|
264
|
+
console.error('❌ Token refresh failed:', err);
|
|
265
|
+
// ✅ Clear everything on refresh failure
|
|
233
266
|
clearToken();
|
|
267
|
+
clearRefreshToken();
|
|
234
268
|
throw err;
|
|
235
269
|
}
|
|
236
270
|
}
|
|
237
271
|
|
|
238
272
|
|
|
273
|
+
|
|
239
274
|
export async function validateCurrentSession() {
|
|
240
275
|
try {
|
|
241
276
|
const { authBaseUrl } = getConfig();
|
package/package.json
CHANGED
package/token.js
CHANGED
|
@@ -1,66 +1,150 @@
|
|
|
1
|
+
|
|
1
2
|
// auth-client/token.js
|
|
2
|
-
let memoryToken = null;
|
|
3
|
-
const listeners = new Set(); // ✅ Add listeners
|
|
4
3
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
import { jwtDecode } from 'jwt-decode';
|
|
5
|
+
|
|
6
|
+
let accessToken = null;
|
|
7
|
+
const listeners = new Set();
|
|
8
|
+
|
|
9
|
+
const REFRESH_COOKIE = 'account_refresh_token';
|
|
10
|
+
const REFRESH_STORAGE_KEY = 'refresh_token'; // localStorage key
|
|
11
|
+
const COOKIE_MAX_AGE = 7 * 24 * 60 * 60; // 7 days in seconds
|
|
12
|
+
|
|
13
|
+
function secureAttribute() {
|
|
9
14
|
try {
|
|
10
|
-
|
|
15
|
+
return typeof window !== 'undefined' && window.location?.protocol === 'https:'
|
|
16
|
+
? '; Secure'
|
|
17
|
+
: '';
|
|
11
18
|
} catch (err) {
|
|
12
|
-
|
|
19
|
+
return '';
|
|
13
20
|
}
|
|
21
|
+
}
|
|
14
22
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
23
|
+
// ========== ACCESS TOKEN (localStorage only) ==========
|
|
24
|
+
function writeAccessToken(token) {
|
|
25
|
+
if (!token) {
|
|
26
|
+
try {
|
|
27
|
+
localStorage.removeItem('authToken');
|
|
28
|
+
} catch (err) {
|
|
29
|
+
console.warn('Could not clear token from localStorage:', err);
|
|
30
|
+
}
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
localStorage.setItem('authToken', token);
|
|
36
|
+
} catch (err) {
|
|
37
|
+
console.warn('Could not persist token to localStorage:', err);
|
|
30
38
|
}
|
|
31
39
|
}
|
|
32
40
|
|
|
33
|
-
|
|
34
|
-
if (memoryToken) return memoryToken;
|
|
41
|
+
function readAccessToken() {
|
|
35
42
|
try {
|
|
36
|
-
|
|
37
|
-
memoryToken = stored;
|
|
38
|
-
return stored;
|
|
43
|
+
return localStorage.getItem('authToken');
|
|
39
44
|
} catch (err) {
|
|
40
45
|
console.warn('Could not read token from localStorage:', err);
|
|
41
46
|
return null;
|
|
42
47
|
}
|
|
43
48
|
}
|
|
44
49
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
50
|
+
// ========== REFRESH TOKEN (localStorage + Cookie dual storage) ==========
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Store refresh token in BOTH localStorage AND cookie
|
|
54
|
+
* Whichever survives (cross-domain, privacy settings) will work
|
|
55
|
+
*/
|
|
56
|
+
export function setRefreshToken(token) {
|
|
57
|
+
if (!token) {
|
|
58
|
+
clearRefreshToken();
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const expires = new Date(Date.now() + COOKIE_MAX_AGE * 1000);
|
|
63
|
+
|
|
64
|
+
// 1. Try to set cookie (works for same-domain, cross-subdomain)
|
|
49
65
|
try {
|
|
50
|
-
|
|
66
|
+
document.cookie = `${REFRESH_COOKIE}=${encodeURIComponent(token)}; Path=/; SameSite=Lax${secureAttribute()}; Expires=${expires.toUTCString()}`;
|
|
67
|
+
console.log('✅ Refresh token stored in cookie');
|
|
51
68
|
} catch (err) {
|
|
52
|
-
console.warn('Could not
|
|
69
|
+
console.warn('⚠️ Could not persist refresh token cookie:', err);
|
|
53
70
|
}
|
|
54
71
|
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
72
|
+
// 2. Also store in localStorage as backup (survives browser privacy settings)
|
|
73
|
+
try {
|
|
74
|
+
localStorage.setItem(REFRESH_STORAGE_KEY, token);
|
|
75
|
+
console.log('✅ Refresh token stored in localStorage');
|
|
76
|
+
} catch (err) {
|
|
77
|
+
console.warn('⚠️ Could not persist refresh token to localStorage:', err);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Get refresh token from cookie OR localStorage (whichever works)
|
|
83
|
+
* Priority: Cookie > localStorage
|
|
84
|
+
*/
|
|
85
|
+
export function getRefreshToken() {
|
|
86
|
+
// 1. Try cookie first (preferred for httpOnly scenario)
|
|
87
|
+
let cookieMatch = null;
|
|
88
|
+
try {
|
|
89
|
+
cookieMatch = document.cookie
|
|
90
|
+
?.split('; ')
|
|
91
|
+
?.find((row) => row.startsWith(`${REFRESH_COOKIE}=`));
|
|
92
|
+
} catch (err) {
|
|
93
|
+
cookieMatch = null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (cookieMatch) {
|
|
97
|
+
const token = decodeURIComponent(cookieMatch.split('=')[1]);
|
|
98
|
+
console.log('✅ Retrieved refresh token from cookie');
|
|
99
|
+
return token;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 2. Fallback to localStorage
|
|
103
|
+
try {
|
|
104
|
+
const token = localStorage.getItem(REFRESH_STORAGE_KEY);
|
|
105
|
+
if (token) {
|
|
106
|
+
console.log('✅ Retrieved refresh token from localStorage (fallback)');
|
|
107
|
+
return token;
|
|
108
|
+
}
|
|
109
|
+
} catch (err) {
|
|
110
|
+
console.warn('⚠️ Could not read refresh token from localStorage:', err);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
console.warn('⚠️ No refresh token found in cookie or localStorage');
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Clear refresh token from BOTH cookie AND localStorage
|
|
119
|
+
*/
|
|
120
|
+
export function clearRefreshToken() {
|
|
121
|
+
// Clear cookie
|
|
122
|
+
try {
|
|
123
|
+
document.cookie = `${REFRESH_COOKIE}=; Path=/; SameSite=Lax${secureAttribute()}; Expires=Thu, 01 Jan 1970 00:00:00 GMT`;
|
|
124
|
+
console.log('✅ Cleared refresh token cookie');
|
|
125
|
+
} catch (err) {
|
|
126
|
+
console.warn('⚠️ Could not clear refresh token cookie:', err);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Clear localStorage
|
|
130
|
+
try {
|
|
131
|
+
localStorage.removeItem(REFRESH_STORAGE_KEY);
|
|
132
|
+
console.log('✅ Cleared refresh token from localStorage');
|
|
133
|
+
} catch (err) {
|
|
134
|
+
console.warn('⚠️ Could not clear refresh token from localStorage:', err);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ========== ACCESS TOKEN MANAGEMENT ==========
|
|
139
|
+
export function setToken(token) {
|
|
140
|
+
const previousToken = accessToken;
|
|
141
|
+
accessToken = token || null;
|
|
142
|
+
writeAccessToken(accessToken);
|
|
143
|
+
|
|
144
|
+
if (previousToken !== accessToken) {
|
|
145
|
+
listeners.forEach((listener) => {
|
|
62
146
|
try {
|
|
63
|
-
listener(
|
|
147
|
+
listener(accessToken, previousToken);
|
|
64
148
|
} catch (err) {
|
|
65
149
|
console.warn('Token listener error:', err);
|
|
66
150
|
}
|
|
@@ -68,82 +152,256 @@ export function clearToken() {
|
|
|
68
152
|
}
|
|
69
153
|
}
|
|
70
154
|
|
|
71
|
-
|
|
155
|
+
export function getToken() {
|
|
156
|
+
if (accessToken) return accessToken;
|
|
157
|
+
accessToken = readAccessToken();
|
|
158
|
+
return accessToken;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function clearToken() {
|
|
162
|
+
if (!accessToken) {
|
|
163
|
+
writeAccessToken(null);
|
|
164
|
+
clearRefreshToken();
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const previousToken = accessToken;
|
|
169
|
+
accessToken = null;
|
|
170
|
+
writeAccessToken(null);
|
|
171
|
+
clearRefreshToken();
|
|
172
|
+
|
|
173
|
+
listeners.forEach((listener) => {
|
|
174
|
+
try {
|
|
175
|
+
listener(null, previousToken);
|
|
176
|
+
} catch (err) {
|
|
177
|
+
console.warn('Token listener error:', err);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ========== HELPER FUNCTIONS ==========
|
|
183
|
+
function decode(token) {
|
|
184
|
+
try {
|
|
185
|
+
return jwtDecode(token);
|
|
186
|
+
} catch (err) {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function isExpired(token, bufferSeconds = 60) {
|
|
192
|
+
if (!token) return true;
|
|
193
|
+
const decoded = decode(token);
|
|
194
|
+
if (!decoded?.exp) return true;
|
|
195
|
+
const now = Date.now() / 1000;
|
|
196
|
+
return decoded.exp < now + bufferSeconds;
|
|
197
|
+
}
|
|
198
|
+
|
|
72
199
|
export function addTokenListener(listener) {
|
|
73
200
|
if (typeof listener !== 'function') {
|
|
74
201
|
throw new Error('Token listener must be a function');
|
|
75
202
|
}
|
|
76
|
-
|
|
77
203
|
listeners.add(listener);
|
|
78
|
-
console.log('📎 Token listener added, total listeners:', listeners.size);
|
|
79
|
-
|
|
80
|
-
// Return cleanup function
|
|
81
204
|
return () => {
|
|
82
|
-
|
|
83
|
-
if (removed) {
|
|
84
|
-
console.log('📎 Token listener removed, remaining listeners:', listeners.size);
|
|
85
|
-
}
|
|
86
|
-
return removed;
|
|
205
|
+
listeners.delete(listener);
|
|
87
206
|
};
|
|
88
207
|
}
|
|
89
208
|
|
|
90
209
|
export function removeTokenListener(listener) {
|
|
91
|
-
|
|
92
|
-
if (removed) {
|
|
93
|
-
console.log('📎 Token listener removed, remaining listeners:', listeners.size);
|
|
94
|
-
}
|
|
95
|
-
return removed;
|
|
96
|
-
|
|
97
|
-
|
|
210
|
+
listeners.delete(listener);
|
|
98
211
|
}
|
|
99
212
|
|
|
100
213
|
export function getListenerCount() {
|
|
101
|
-
return listeners.size;
|
|
214
|
+
return listeners.size;
|
|
102
215
|
}
|
|
103
216
|
|
|
217
|
+
export function isAuthenticated() {
|
|
218
|
+
const token = getToken();
|
|
219
|
+
return !!token && !isExpired(token, 15);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
|
|
104
223
|
|
|
224
|
+
// // auth-client/token.js
|
|
225
|
+
// import { jwtDecode } from 'jwt-decode';
|
|
226
|
+
|
|
227
|
+
// let accessToken = null;
|
|
228
|
+
// const listeners = new Set();
|
|
229
|
+
|
|
230
|
+
// const REFRESH_COOKIE = 'account_refresh_token';
|
|
231
|
+
// const COOKIE_MAX_AGE = 7 * 24 * 60 * 60; // 7 days in seconds
|
|
232
|
+
|
|
233
|
+
// function secureAttribute() {
|
|
234
|
+
// try {
|
|
235
|
+
// return typeof window !== 'undefined' && window.location?.protocol === 'https:'
|
|
236
|
+
// ? '; Secure'
|
|
237
|
+
// : '';
|
|
238
|
+
// } catch (err) {
|
|
239
|
+
// return '';
|
|
240
|
+
// }
|
|
241
|
+
// }
|
|
242
|
+
|
|
243
|
+
// function writeAccessToken(token) {
|
|
244
|
+
// if (!token) {
|
|
245
|
+
// try {
|
|
246
|
+
// localStorage.removeItem('authToken');
|
|
247
|
+
// } catch (err) {
|
|
248
|
+
// console.warn('Could not clear token from localStorage:', err);
|
|
249
|
+
// }
|
|
250
|
+
// return;
|
|
251
|
+
// }
|
|
252
|
+
|
|
253
|
+
// try {
|
|
254
|
+
// localStorage.setItem('authToken', token);
|
|
255
|
+
// } catch (err) {
|
|
256
|
+
// console.warn('Could not persist token to localStorage:', err);
|
|
257
|
+
// }
|
|
258
|
+
// }
|
|
105
259
|
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
//
|
|
260
|
+
// function readAccessToken() {
|
|
261
|
+
// try {
|
|
262
|
+
// return localStorage.getItem('authToken');
|
|
263
|
+
// } catch (err) {
|
|
264
|
+
// console.warn('Could not read token from localStorage:', err);
|
|
265
|
+
// return null;
|
|
266
|
+
// }
|
|
267
|
+
// }
|
|
110
268
|
|
|
269
|
+
// function decode(token) {
|
|
111
270
|
// try {
|
|
112
|
-
//
|
|
113
|
-
// if (!base64Url) return null;
|
|
114
|
-
|
|
115
|
-
// const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
|
|
116
|
-
// const jsonPayload = decodeURIComponent(
|
|
117
|
-
// atob(base64)
|
|
118
|
-
// .split('')
|
|
119
|
-
// .map((c) => `%${('00' + c.charCodeAt(0).toString(16)).slice(-2)}`)
|
|
120
|
-
// .join('')
|
|
121
|
-
// );
|
|
122
|
-
|
|
123
|
-
// return JSON.parse(jsonPayload);
|
|
271
|
+
// return jwtDecode(token);
|
|
124
272
|
// } catch (err) {
|
|
125
|
-
// console.warn('Could not decode token:', err);
|
|
126
273
|
// return null;
|
|
127
274
|
// }
|
|
128
275
|
// }
|
|
129
276
|
|
|
130
|
-
//
|
|
131
|
-
// export function isTokenExpired(token, bufferSeconds = 0) {
|
|
277
|
+
// function isExpired(token, bufferSeconds = 60) {
|
|
132
278
|
// if (!token) return true;
|
|
279
|
+
// const decoded = decode(token);
|
|
280
|
+
// if (!decoded?.exp) return true;
|
|
281
|
+
// const now = Date.now() / 1000;
|
|
282
|
+
// return decoded.exp < now + bufferSeconds;
|
|
283
|
+
// }
|
|
133
284
|
|
|
134
|
-
//
|
|
135
|
-
//
|
|
285
|
+
// export function setToken(token) {
|
|
286
|
+
// const previousToken = accessToken;
|
|
287
|
+
// accessToken = token || null;
|
|
288
|
+
// writeAccessToken(accessToken);
|
|
136
289
|
|
|
137
|
-
//
|
|
138
|
-
//
|
|
290
|
+
// if (previousToken !== accessToken) {
|
|
291
|
+
// listeners.forEach((listener) => {
|
|
292
|
+
// try {
|
|
293
|
+
// listener(accessToken, previousToken);
|
|
294
|
+
// } catch (err) {
|
|
295
|
+
// console.warn('Token listener error:', err);
|
|
296
|
+
// }
|
|
297
|
+
// });
|
|
298
|
+
// }
|
|
299
|
+
// }
|
|
139
300
|
|
|
140
|
-
//
|
|
141
|
-
//
|
|
301
|
+
// export function getToken() {
|
|
302
|
+
// if (accessToken) return accessToken;
|
|
303
|
+
// accessToken = readAccessToken();
|
|
304
|
+
// return accessToken;
|
|
142
305
|
// }
|
|
143
306
|
|
|
144
|
-
//
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
307
|
+
// export function clearToken() {
|
|
308
|
+
// if (!accessToken) {
|
|
309
|
+
// writeAccessToken(null);
|
|
310
|
+
// clearRefreshToken();
|
|
311
|
+
// return;
|
|
312
|
+
// }
|
|
313
|
+
|
|
314
|
+
// const previousToken = accessToken;
|
|
315
|
+
// accessToken = null;
|
|
316
|
+
// writeAccessToken(null);
|
|
317
|
+
// clearRefreshToken();
|
|
318
|
+
|
|
319
|
+
// listeners.forEach((listener) => {
|
|
320
|
+
// try {
|
|
321
|
+
// listener(null, previousToken);
|
|
322
|
+
// } catch (err) {
|
|
323
|
+
// console.warn('Token listener error:', err);
|
|
324
|
+
// }
|
|
325
|
+
// });
|
|
326
|
+
// }
|
|
327
|
+
|
|
328
|
+
// export function setRefreshToken(token) {
|
|
329
|
+
// if (!token) {
|
|
330
|
+
// clearRefreshToken();
|
|
331
|
+
// return;
|
|
332
|
+
// }
|
|
333
|
+
|
|
334
|
+
// const expires = new Date(Date.now() + COOKIE_MAX_AGE * 1000);
|
|
335
|
+
// try {
|
|
336
|
+
// document.cookie = `${REFRESH_COOKIE}=${encodeURIComponent(token)}; Path=/; SameSite=Strict${secureAttribute()}; Expires=${expires.toUTCString()}`;
|
|
337
|
+
// } catch (err) {
|
|
338
|
+
// console.warn('Could not persist refresh token cookie:', err);
|
|
339
|
+
// }
|
|
340
|
+
|
|
341
|
+
// try {
|
|
342
|
+
// sessionStorage.setItem(REFRESH_COOKIE, token);
|
|
343
|
+
// } catch (err) {
|
|
344
|
+
// console.warn('Could not persist refresh token to sessionStorage:', err);
|
|
345
|
+
// }
|
|
346
|
+
// }
|
|
347
|
+
|
|
348
|
+
// export function getRefreshToken() {
|
|
349
|
+
// // Prefer cookie to align with server expectations
|
|
350
|
+
// let cookieMatch = null;
|
|
351
|
+
|
|
352
|
+
// try {
|
|
353
|
+
// cookieMatch = document.cookie
|
|
354
|
+
// ?.split('; ')
|
|
355
|
+
// ?.find((row) => row.startsWith(`${REFRESH_COOKIE}=`));
|
|
356
|
+
// } catch (err) {
|
|
357
|
+
// cookieMatch = null;
|
|
358
|
+
// }
|
|
359
|
+
|
|
360
|
+
// if (cookieMatch) {
|
|
361
|
+
// return decodeURIComponent(cookieMatch.split('=')[1]);
|
|
362
|
+
// }
|
|
363
|
+
|
|
364
|
+
// try {
|
|
365
|
+
// return sessionStorage.getItem(REFRESH_COOKIE);
|
|
366
|
+
// } catch (err) {
|
|
367
|
+
// console.warn('Could not read refresh token from sessionStorage:', err);
|
|
368
|
+
// return null;
|
|
369
|
+
// }
|
|
370
|
+
// }
|
|
371
|
+
|
|
372
|
+
// export function clearRefreshToken() {
|
|
373
|
+
// try {
|
|
374
|
+
// document.cookie = `${REFRESH_COOKIE}=; Path=/; SameSite=Strict${secureAttribute()}; Expires=Thu, 01 Jan 1970 00:00:00 GMT`;
|
|
375
|
+
// } catch (err) {
|
|
376
|
+
// console.warn('Could not clear refresh token cookie:', err);
|
|
377
|
+
// }
|
|
378
|
+
// try {
|
|
379
|
+
// sessionStorage.removeItem(REFRESH_COOKIE);
|
|
380
|
+
// } catch (err) {
|
|
381
|
+
// console.warn('Could not clear refresh token from sessionStorage:', err);
|
|
382
|
+
// }
|
|
383
|
+
// }
|
|
384
|
+
|
|
385
|
+
// export function addTokenListener(listener) {
|
|
386
|
+
// if (typeof listener !== 'function') {
|
|
387
|
+
// throw new Error('Token listener must be a function');
|
|
388
|
+
// }
|
|
389
|
+
// listeners.add(listener);
|
|
390
|
+
// return () => {
|
|
391
|
+
// listeners.delete(listener);
|
|
392
|
+
// };
|
|
393
|
+
// }
|
|
394
|
+
|
|
395
|
+
// export function removeTokenListener(listener) {
|
|
396
|
+
// listeners.delete(listener);
|
|
397
|
+
// }
|
|
398
|
+
|
|
399
|
+
// export function getListenerCount() {
|
|
400
|
+
// return listeners.size;
|
|
401
|
+
// }
|
|
402
|
+
|
|
403
|
+
// export function isAuthenticated() {
|
|
404
|
+
// const token = getToken();
|
|
405
|
+
// return !!token && !isExpired(token, 15);
|
|
406
|
+
// }
|
|
149
407
|
|