Skip to content

Commit 3f3ce44

Browse files
committed
Merge branch 'main' into Fix-screen-shot-not-trigger
2 parents dc6a6b8 + aae0892 commit 3f3ce44

File tree

8 files changed

+347
-11
lines changed

8 files changed

+347
-11
lines changed

backend/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
"@huggingface/transformers": "latest",
3434
"@nestjs-modules/mailer": "^2.0.2",
3535
"@nestjs/apollo": "^12.2.0",
36-
"@nestjs/axios": "^3.0.3",
36+
"@nestjs/axios": "^3.1.3",
3737
"@nestjs/common": "^10.0.0",
3838
"@nestjs/config": "^3.2.3",
3939
"@nestjs/core": "^10.0.0",
@@ -48,8 +48,8 @@
4848
"@types/normalize-path": "^3.0.2",
4949
"@types/toposort": "^2.0.7",
5050
"archiver": "^7.0.1",
51+
"axios": "^1.8.3",
5152
"aws-sdk": "^2.1692.0",
52-
"axios": "^1.7.7",
5353
"bcrypt": "^5.1.1",
5454
"class-transformer": "^0.5.1",
5555
"class-validator": "^0.14.1",

backend/src/auth/auth.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ export class AuthService {
217217
);
218218

219219
const refreshTokenEntity = await this.createRefreshToken(user);
220-
this.jwtCacheService.storeAccessToken(refreshTokenEntity.token);
220+
this.jwtCacheService.storeAccessToken(accessToken);
221221

222222
return {
223223
accessToken,

frontend/src/api/ChatStreamAPI.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { ChatInputType } from '@/graphql/type';
2+
import authenticatedFetch from '@/lib/authenticatedFetch';
23

34
export const startChatStream = async (
45
input: ChatInputType,
@@ -9,7 +10,7 @@ export const startChatStream = async (
910
throw new Error('Not authenticated');
1011
}
1112
const { chatId, message, model } = input;
12-
const response = await fetch('/api/chat', {
13+
const response = await authenticatedFetch('/api/chat', {
1314
method: 'POST',
1415
headers: {
1516
'Content-Type': 'application/json',

frontend/src/components/chat/code-engine/responsive-toolbar.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { logger } from '@/app/log/logger';
1717
import { useMutation, useQuery, gql } from '@apollo/client';
1818
import { toast } from 'sonner';
1919
import { SYNC_PROJECT_TO_GITHUB, GET_PROJECT } from '../../../graphql/request';
20+
import { authenticatedFetch } from '@/lib/authenticatedFetch';
2021

2122
interface ResponsiveToolbarProps {
2223
isLoading: boolean;
@@ -222,9 +223,14 @@ const ResponsiveToolbar = ({
222223
}
223224

224225
// Fetch with credentials to ensure auth is included
225-
const response = await fetch(downloadUrl, {
226+
// const response = await fetch(downloadUrl, {
227+
// method: 'GET',
228+
// headers: headers,
229+
// });
230+
231+
// Use authenticatedFetch which handles token refresh
232+
const response = await authenticatedFetch(downloadUrl, {
226233
method: 'GET',
227-
headers: headers,
228234
});
229235

230236
if (!response.ok) {
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
import { LocalStore } from '@/lib/storage';
2+
import { client } from '@/lib/client';
3+
import { REFRESH_TOKEN_MUTATION } from '@/graphql/mutations/auth';
4+
import { gql } from '@apollo/client';
5+
6+
// Prevent multiple simultaneous refresh attempts
7+
let isRefreshing = false;
8+
let refreshPromise: Promise<string | null> | null = null;
9+
10+
/**
11+
* Refreshes the access token using the refresh token
12+
* @returns Promise that resolves to the new token or null if refresh failed
13+
*/
14+
export const refreshAccessToken = async (): Promise<string | null> => {
15+
// If a refresh is already in progress, return that promise
16+
if (isRefreshing && refreshPromise) {
17+
return refreshPromise;
18+
}
19+
20+
isRefreshing = true;
21+
refreshPromise = (async () => {
22+
try {
23+
const refreshToken = localStorage.getItem(LocalStore.refreshToken);
24+
if (!refreshToken) {
25+
return null;
26+
}
27+
28+
// Use Apollo client to refresh the token
29+
const result = await client.mutate({
30+
mutation: REFRESH_TOKEN_MUTATION,
31+
variables: { refreshToken },
32+
});
33+
34+
if (result.data?.refreshToken?.accessToken) {
35+
const newAccessToken = result.data.refreshToken.accessToken;
36+
const newRefreshToken =
37+
result.data.refreshToken.refreshToken || refreshToken;
38+
39+
localStorage.setItem(LocalStore.accessToken, newAccessToken);
40+
localStorage.setItem(LocalStore.refreshToken, newRefreshToken);
41+
42+
console.log('Token refreshed successfully');
43+
return newAccessToken;
44+
}
45+
46+
return null;
47+
} catch (error) {
48+
console.error('Error refreshing token:', error);
49+
return null;
50+
} finally {
51+
isRefreshing = false;
52+
refreshPromise = null;
53+
}
54+
})();
55+
56+
return refreshPromise;
57+
};
58+
59+
/**
60+
* Fetch wrapper that handles authentication and token refresh
61+
* @param url The URL to fetch
62+
* @param options Fetch options
63+
* @param retryOnAuth Whether to retry on 401 errors (default: true)
64+
* @returns Response from the fetch request
65+
*/
66+
export const authenticatedFetch = async (
67+
url: string,
68+
options: RequestInit = {},
69+
retryOnAuth: boolean = true
70+
): Promise<Response> => {
71+
// Get current token
72+
const token = localStorage.getItem(LocalStore.accessToken);
73+
74+
// Setup headers with authentication
75+
const headers = new Headers(options.headers || {});
76+
if (token) {
77+
headers.set('Authorization', `Bearer ${token}`);
78+
}
79+
80+
// Make the request
81+
const response = await fetch(url, {
82+
...options,
83+
headers,
84+
});
85+
86+
// If we get a 401 and we should retry, attempt to refresh the token
87+
if (response.status === 401 && retryOnAuth) {
88+
const newToken = await refreshAccessToken();
89+
90+
if (newToken) {
91+
// Update the authorization header with the new token
92+
headers.set('Authorization', `Bearer ${newToken}`);
93+
94+
// Retry the request with the new token
95+
return fetch(url, {
96+
...options,
97+
headers,
98+
});
99+
} else {
100+
// If refresh failed, redirect to home/login
101+
if (typeof window !== 'undefined') {
102+
localStorage.removeItem(LocalStore.accessToken);
103+
localStorage.removeItem(LocalStore.refreshToken);
104+
window.location.href = '/';
105+
}
106+
}
107+
}
108+
109+
return response;
110+
};
111+
112+
/**
113+
* Processes a streaming response from a server-sent events endpoint
114+
* @param response Fetch Response object (must be a streaming response)
115+
* @param onChunk Optional callback to process each chunk as it arrives
116+
* @returns Promise with the full aggregated content
117+
*/
118+
export const processStreamResponse = async (
119+
response: Response,
120+
onChunk?: (chunk: string) => void
121+
): Promise<string> => {
122+
if (!response.body) {
123+
throw new Error('Response has no body');
124+
}
125+
126+
const reader = response.body.getReader();
127+
let fullContent = '';
128+
let isStreamDone = false;
129+
130+
try {
131+
// More explicit condition than while(true)
132+
while (!isStreamDone) {
133+
const { done, value } = await reader.read();
134+
135+
if (done) {
136+
isStreamDone = true;
137+
continue;
138+
}
139+
140+
const text = new TextDecoder().decode(value);
141+
const lines = text.split('\n\n');
142+
143+
for (const line of lines) {
144+
if (line.startsWith('data: ')) {
145+
const data = line.slice(6).trim();
146+
147+
// Additional exit condition
148+
if (data === '[DONE]') {
149+
isStreamDone = true;
150+
break;
151+
}
152+
153+
try {
154+
const parsed = JSON.parse(data);
155+
if (parsed.content) {
156+
fullContent += parsed.content;
157+
if (onChunk) {
158+
onChunk(parsed.content);
159+
}
160+
}
161+
} catch (e) {
162+
console.error('Error parsing SSE data:', e);
163+
}
164+
}
165+
}
166+
}
167+
168+
return fullContent;
169+
} catch (error) {
170+
console.error('Error reading stream:', error);
171+
throw error;
172+
} finally {
173+
// Ensure we clean up the reader if we exit due to an error
174+
if (!isStreamDone) {
175+
reader
176+
.cancel()
177+
.catch((e) => console.error('Error cancelling reader:', e));
178+
}
179+
}
180+
};
181+
182+
export default authenticatedFetch;

0 commit comments

Comments
 (0)