Skip to content

Commit a10ea02

Browse files
author
Jicheng Lu
committed
temp save
1 parent 72eeb2c commit a10ea02

File tree

8 files changed

+205
-39
lines changed

8 files changed

+205
-39
lines changed

src/lib/common/ProfileDropdown.svelte

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<script>
22
import { Dropdown, DropdownToggle, DropdownMenu, DropdownItem } from '@sveltestrap/sveltestrap';
3-
import { resetLocalStorage } from '$lib/helpers/store';
3+
import { resetStorage } from '$lib/helpers/store';
44
import { goto } from '$app/navigation';
55
import { browser } from '$app/environment';
66
import { userStore } from '$lib/helpers/store';
@@ -15,7 +15,7 @@
1515
1616
function logout() {
1717
if (browser){
18-
resetLocalStorage(true);
18+
resetStorage(true);
1919
}
2020
2121
const chatFrame = document.getElementById(CHAT_FRAME_ID);

src/lib/helpers/http.js

Lines changed: 128 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,99 @@
11
import axios from 'axios';
22
import { getUserStore, globalErrorStore, loaderStore } from '$lib/helpers/store.js';
3+
import { renewToken } from '$lib/services/auth-service';
4+
5+
// Refresh handling state
6+
let isRefreshing = false;
7+
8+
/** @type {{config: import('axios').InternalAxiosRequestConfig, resolve: (value:any)=>void, reject: (reason?: any)=>void}[]} */
9+
const failedQueue = [];
10+
11+
/** @type {{config: import('axios').InternalAxiosRequestConfig, resolve: (value:any)=>void, reject: (reason?: any)=>void}[]} */
12+
const pendingRequestQueue = [];
13+
14+
/**
15+
* Wrap renewToken into a Promise that resolves with the new access token string
16+
* @param {string} token
17+
* @returns {Promise<string>}
18+
*/
19+
function refreshAccessToken(token) {
20+
return new Promise((resolve, reject) => {
21+
renewToken(token, (newToken) => resolve(newToken), () => reject(new Error('Failed to refresh token')));
22+
});
23+
}
24+
25+
/**
26+
* Retry queued requests sequentially with the provided token
27+
* @param {string} newToken
28+
* @returns {Promise<void>}
29+
*/
30+
function processQueueSequentially(newToken) {
31+
let chain = Promise.resolve();
32+
while (failedQueue.length) {
33+
const item = failedQueue.shift();
34+
if (!item) continue;
35+
const { config, resolve, reject } = item;
36+
// Ensure headers exists; Axios may use AxiosHeaders type
37+
// @ts-ignore
38+
config.headers = config.headers || {};
39+
// @ts-ignore
40+
config.headers.Authorization = `Bearer ${newToken}`;
41+
chain = chain.then(() => axios(config).then(resolve).catch(reject));
42+
}
43+
return chain;
44+
}
345

446
// Add a request interceptor to attach authentication tokens or headers
547
axios.interceptors.request.use(
648
(config) => {
7-
// Add your authentication logic here
849
const user = getUserStore();
950
if (!skipLoader(config)) {
1051
loaderStore.set(true);
1152
}
12-
// For example, attach an authentication token to the request headers
13-
if (user.token)
53+
54+
// Proactive token refresh: if expired or a refresh is in progress,
55+
// queue this request until a new token is available
56+
if (isTokenExired() || isRefreshing) {
57+
return new Promise((resolve, reject) => {
58+
pendingRequestQueue.push({ config, resolve, reject });
59+
60+
if (!isRefreshing) {
61+
isRefreshing = true;
62+
refreshAccessToken(user?.token || '')
63+
.then((newToken) => {
64+
isRefreshing = false;
65+
// Release queued requests with the new token
66+
while (pendingRequestQueue.length) {
67+
const item = pendingRequestQueue.shift();
68+
if (!item) continue;
69+
const { config: cfg, resolve: res } = item;
70+
// @ts-ignore
71+
cfg.headers = cfg.headers || {};
72+
// @ts-ignore
73+
cfg.headers.Authorization = `Bearer ${newToken}`;
74+
res(cfg);
75+
}
76+
})
77+
.catch((err) => {
78+
isRefreshing = false;
79+
// Reject queued requests and redirect to login
80+
while (pendingRequestQueue.length) {
81+
const item = pendingRequestQueue.shift();
82+
if (item) item.reject(err);
83+
}
84+
redirectToLogin();
85+
});
86+
}
87+
});
88+
}
89+
90+
// Attach current token if present
91+
if (user.token) {
92+
// @ts-ignore
93+
config.headers = config.headers || {};
94+
// @ts-ignore
1495
config.headers.Authorization = `Bearer ${user.token}`;
96+
}
1597
return config;
1698
},
1799
(error) => {
@@ -25,23 +107,46 @@ axios.interceptors.response.use(
25107
(response) => {
26108
// If the request was successful, return the response
27109
loaderStore.set(false);
28-
const user = getUserStore();
29-
const isExpired = Date.now() / 1000 > user.expires;
30-
if (isExpired || response?.status === 401) {
31-
redirectToLogin();
32-
return Promise.reject('user token expired!');
33-
}
34110
return response;
35111
},
36112
(error) => {
37113
loaderStore.set(false);
38-
const user = getUserStore();
39-
40-
const isExpired = Date.now() / 1000 > user.expires;
41-
if (isExpired || error?.response?.status === 401) {
42-
redirectToLogin();
43-
return Promise.reject(error);
44-
} else if (!skipGlobalError(error.config)) {
114+
const originalRequest = error?.config;
115+
116+
// If token expired or 401 returned, attempt a single token refresh and
117+
// retry failed requests in sequence.
118+
if ((error?.response?.status === 401 || isTokenExired()) && originalRequest && !originalRequest._retry) {
119+
originalRequest._retry = true;
120+
121+
return new Promise((resolve, reject) => {
122+
// Push the current request into the queue
123+
failedQueue.push({ config: originalRequest, resolve, reject });
124+
125+
// Start refresh if not already in progress
126+
if (!isRefreshing) {
127+
isRefreshing = true;
128+
const user = getUserStore();
129+
130+
refreshAccessToken(user?.token || '')
131+
.then((newToken) => {
132+
isRefreshing = false;
133+
return processQueueSequentially(newToken);
134+
})
135+
.catch((err) => {
136+
isRefreshing = false;
137+
138+
// Reject all queued requests
139+
while (failedQueue.length) {
140+
const item = failedQueue.shift();
141+
if (item) item.reject(err);
142+
}
143+
144+
redirectToLogin();
145+
throw err;
146+
});
147+
}
148+
});
149+
} else if (!skipGlobalError(originalRequest)) {
45150
globalErrorStore.set(true);
46151
setTimeout(() => {
47152
globalErrorStore.set(false);
@@ -53,6 +158,10 @@ axios.interceptors.response.use(
53158
}
54159
);
55160

161+
function isTokenExired() {
162+
const user = getUserStore();
163+
return Date.now() / 1000 > user.expires;
164+
}
56165

57166
function redirectToLogin() {
58167
const curUrl = window.location.pathname + window.location.search;
@@ -155,7 +264,7 @@ function skipGlobalError(config) {
155264
new RegExp('http(s*)://(.*?)/conversation/(.*?)/update-message', 'g'),
156265
new RegExp('http(s*)://(.*?)/conversation/(.*?)/update-tags', 'g')
157266
];
158-
267+
159268
/** @type {RegExp[]} */
160269
const deleteRegexes = [
161270
new RegExp('http(s*)://(.*?)/knowledge/vector/(.*?)/delete-collection', 'g'),
@@ -201,7 +310,7 @@ export function replaceUrl(url, args) {
201310

202311
/**
203312
* Replace new line as <br>
204-
* @param {string} text
313+
* @param {string} text
205314
* @returns string
206315
*/
207316
export function replaceNewLine(text) {
@@ -210,7 +319,7 @@ export function replaceNewLine(text) {
210319

211320
/**
212321
* Replace unnecessary markdown
213-
* @param {string} text
322+
* @param {string} text
214323
* @returns {string}
215324
*/
216325
export function replaceMarkdown(text) {

src/lib/helpers/store.js

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export const userStore = writable({ id: "", full_name: "", expires: 0, token: nu
4141
export function getUserStore() {
4242
if (browser) {
4343
// Access localStorage only if in the browser context
44-
let json = localStorage.getItem(userKey);
44+
let json = sessionStorage.getItem(userKey);
4545
if (json)
4646
return JSON.parse(json);
4747
else
@@ -54,7 +54,7 @@ export function getUserStore() {
5454

5555
userStore.subscribe(value => {
5656
if (browser && value.token) {
57-
localStorage.setItem(userKey, JSON.stringify(value));
57+
sessionStorage.setItem(userKey, JSON.stringify(value));
5858
}
5959
});
6060

@@ -219,13 +219,31 @@ const createKnowledgeBaseDocumentStore = () => {
219219
export const knowledgeBaseDocumentStore = createKnowledgeBaseDocumentStore();
220220

221221

222-
export function resetLocalStorage(resetUser = false) {
222+
export function resetStorage(resetUser = false) {
223223
conversationUserStateStore.resetAll();
224224
conversationUserMessageStore.reset();
225225
conversationUserAttachmentStore.reset();
226-
localStorage.removeItem('conversation');
226+
clearLocalStorage(['message']);
227227

228228
if (resetUser) {
229-
localStorage.removeItem('user');
229+
sessionStorage.removeItem(userKey);
230+
}
231+
}
232+
233+
/** @param {string[]?} keyPrefixes */
234+
function clearLocalStorage(keyPrefixes = null) {
235+
if (!keyPrefixes) {
236+
localStorage.clear();
237+
return;
238+
}
239+
240+
for (let i = 0; i < localStorage.length; i++) {
241+
const key = localStorage.key(i);
242+
if (!key) continue;
243+
244+
const found = keyPrefixes.find(x => key.startsWith(x));
245+
if (found) {
246+
localStorage.removeItem(key);
247+
}
230248
}
231249
}

src/lib/services/api-endpoints.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export const endpoints = {
1010

1111
// user
1212
tokenUrl: `${host}/token`,
13+
renewTokenUrl: `${host}/renew-token`,
1314
myInfoUrl: `${host}/user/me`,
1415
usersUrl: `${host}/users`,
1516
userDetailUrl: `${host}/user/{id}/details`,

src/lib/services/auth-service.js

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export async function getToken(email, password, onSucceed, onError) {
2929
if (!result) {
3030
return;
3131
}
32-
let user = getUserStore();
32+
const user = getUserStore();
3333
user.token = result.access_token;
3434
user.expires = result.expires;
3535
userStore.set(user);
@@ -40,6 +40,42 @@ export async function getToken(email, password, onSucceed, onError) {
4040
});
4141
}
4242

43+
/**
44+
* @param {string} token
45+
* @param {((arg0: string) => void) | null} [onSucceed]
46+
* @param {(() => void) | null} [onError]
47+
*/
48+
export async function renewToken(token, onSucceed = null, onError = null) {
49+
const headers = {
50+
Authorization: `Bearer ${token}`,
51+
};
52+
53+
await fetch(endpoints.renewTokenUrl, {
54+
method: 'POST',
55+
headers: headers,
56+
}).then(response => {
57+
if (response.ok) {
58+
return response.json();
59+
} else {
60+
console.log(response.statusText);
61+
onError?.();
62+
return false;
63+
}
64+
}).then(result => {
65+
if (!result) {
66+
return;
67+
}
68+
const user = getUserStore();
69+
user.token = result.access_token;
70+
user.expires = result.expires;
71+
userStore.set(user);
72+
onSucceed?.(result.access_token);
73+
})
74+
.catch(() => {
75+
onError?.();
76+
});
77+
}
78+
4379
/**
4480
* Set token from exteranl
4581
* @param {string} token

src/routes/(authentication)/login/+page.svelte

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
PUBLIC_AUTH_ENABLE_FIND_PWD,
3232
} from '$env/static/public';
3333
import { onMount } from 'svelte';
34-
import { resetLocalStorage } from '$lib/helpers/store';
34+
import { resetStorage } from '$lib/helpers/store';
3535
3636
let username = PUBLIC_ADMIN_USERNAME;
3737
let password = PUBLIC_ADMIN_PASSWORD;
@@ -45,7 +45,7 @@
4545
const userName = localStorage.getItem('user_name');
4646
isRememberMe = userName !== null;
4747
if(isRememberMe){
48-
username = userName;
48+
username = userName || '';
4949
}
5050
});
5151
function handleRememberMe(){
@@ -56,6 +56,8 @@
5656
localStorage.removeItem("user_name");
5757
}
5858
}
59+
60+
/** @param {any} e */
5961
async function onSubmit(e) {
6062
isSubmitting = true;
6163
handleRememberMe();
@@ -66,7 +68,7 @@
6668
status = 'success';
6769
const redirectUrl = $page.url.searchParams.get('redirect');
6870
isSubmitting = false;
69-
resetLocalStorage();
71+
resetStorage();
7072
if (redirectUrl) {
7173
window.location.href = decodeURIComponent(redirectUrl);
7274
} else {
@@ -87,16 +89,16 @@
8789
}
8890
8991
function onPasswordToggle() {
90-
var x = document.getElementById('user-password');
92+
const x = document.getElementById('user-password');
9193
if (!x) return;
9294
9395
if (x.type === 'password') {
9496
x.type = 'text';
95-
var icon = document.getElementById('password-eye-icon');
97+
const icon = document.getElementById('password-eye-icon');
9698
icon.className = 'mdi mdi-eye-off-outline';
9799
} else {
98100
x.type = 'password';
99-
var icon = document.getElementById('password-eye-icon');
101+
const icon = document.getElementById('password-eye-icon');
100102
icon.className = 'mdi mdi-eye-outline';
101103
}
102104
}

src/routes/(authentication)/register/+page.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
const data = await response.json();
3333
3434
if (response.ok && data.message === 'success') {
35-
localStorage.setItem('authUser', JSON.stringify(data));
35+
sessionStorage.setItem('authUser', JSON.stringify(data));
3636
isOpen = true;
3737
msg = 'Registration success. Redirecting...';
3838
status = 'success';

0 commit comments

Comments
 (0)