Improve error handling, automatically renew access token
This commit is contained in:
@@ -6,10 +6,93 @@ import type { Tag } from "~/types/tag";
|
||||
|
||||
const ENDPOINT = 'http://localhost:5259/api';
|
||||
|
||||
// This is a simplified session renewal handler. It's outside the class
|
||||
// to avoid circular dependencies or complex context passing.
|
||||
// It directly interacts with localStorage, which is where tokens are stored.
|
||||
const renewSessionAndRetry = async (failedRequest: () => Promise<Response>): Promise<Response> => {
|
||||
const refreshToken = localStorage.getItem('refreshToken');
|
||||
if (!refreshToken) return failedRequest();
|
||||
|
||||
try {
|
||||
const renewResponse = await fetch(`${ENDPOINT}/auth/renew`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ refreshToken }),
|
||||
});
|
||||
|
||||
if (!renewResponse.ok) {
|
||||
// If renew fails, logout by clearing storage
|
||||
localStorage.removeItem('user');
|
||||
localStorage.removeItem('accessToken');
|
||||
localStorage.removeItem('refreshToken');
|
||||
// Propagate the original failure, the app will react to the user being logged out.
|
||||
return failedRequest();
|
||||
}
|
||||
|
||||
const { accessToken, refreshToken: newRefreshToken } = await renewResponse.json();
|
||||
|
||||
// Update tokens in localStorage
|
||||
localStorage.setItem('accessToken', accessToken);
|
||||
localStorage.setItem('refreshToken', newRefreshToken);
|
||||
|
||||
// Update user object in localStorage
|
||||
const userString = localStorage.getItem('user');
|
||||
if (userString) {
|
||||
const user = JSON.parse(userString);
|
||||
user.accessToken = accessToken;
|
||||
user.refreshToken = newRefreshToken;
|
||||
localStorage.setItem('user', JSON.stringify(user));
|
||||
}
|
||||
|
||||
// Retry the original request with the new token.
|
||||
// We assume the original request function knows how to get the new token from storage.
|
||||
return await failedRequest();
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error during token renewal:", error);
|
||||
// Logout on any renewal error
|
||||
localStorage.removeItem('user');
|
||||
localStorage.removeItem('accessToken');
|
||||
localStorage.removeItem('refreshToken');
|
||||
return failedRequest();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export class API {
|
||||
|
||||
private static async _request(url: string, options: RequestInit): Promise<Response> {
|
||||
// Add Authorization header if an access token is available
|
||||
const token = localStorage.getItem('accessToken');
|
||||
if (token) {
|
||||
options.headers = {
|
||||
...options.headers,
|
||||
'Authorization': `Bearer ${token}`,
|
||||
};
|
||||
}
|
||||
|
||||
let response = await fetch(url, options);
|
||||
|
||||
// If response is 401, try to renew the token and retry the request once.
|
||||
if (response.status === 401) {
|
||||
console.log("Access token expired. Attempting to renew...");
|
||||
// To retry, we need to create a function that can be called again after renewal.
|
||||
const retryRequest = () => {
|
||||
const newToken = localStorage.getItem('accessToken');
|
||||
const newOptions = { ...options };
|
||||
if (newToken) {
|
||||
newOptions.headers = { ...newOptions.headers, 'Authorization': `Bearer ${newToken}` };
|
||||
}
|
||||
return fetch(url, newOptions);
|
||||
};
|
||||
return await renewSessionAndRetry(retryRequest);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
private static async genericRequest(url: string, method: string, body: any = null, headers: Record<string, string> = {}): Promise<Response> {
|
||||
const localHeaders = {
|
||||
const localHeaders: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
...headers,
|
||||
};
|
||||
@@ -20,14 +103,14 @@ export class API {
|
||||
if (body) {
|
||||
options.body = JSON.stringify(body);
|
||||
}
|
||||
const response = await fetch(url, options);
|
||||
return response;
|
||||
return API._request(url, options);
|
||||
}
|
||||
|
||||
private static async get(url: string, headers: Record<string, string> = {}) {
|
||||
const response = await fetch(url);
|
||||
const response = await API._request(url, { headers });
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
const errorData = await response.json().catch(() => ({ detail: `HTTP error! status: ${response.status}` }));
|
||||
throw { ...errorData, status: response.status };
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
@@ -51,15 +134,15 @@ export class API {
|
||||
}
|
||||
|
||||
public static async createTag(name: string, tagType: string, user: User): Promise<Response> {
|
||||
return await API.post(`${ENDPOINT}/tags`, { name, type: tagType }, { Authorization: `Bearer ${user.accessToken}`});
|
||||
return await API.post(`${ENDPOINT}/tags`, { name, type: tagType });
|
||||
}
|
||||
|
||||
public static async updateTag(name: string, newType: string, user: User): Promise<Response> {
|
||||
return await API.patch(`${ENDPOINT}/tags/${encodeURIComponent(name)}`, { type: newType }, { Authorization: `Bearer ${user.accessToken}`});
|
||||
return await API.patch(`${ENDPOINT}/tags/${encodeURIComponent(name)}`, { type: newType });
|
||||
}
|
||||
|
||||
public static async deleteTag(name: string, user: User): Promise<Response> {
|
||||
return await API.delete(`${ENDPOINT}/tags/${encodeURIComponent(name)}`, {}, { Authorization: `Bearer ${user.accessToken}`});
|
||||
return await API.delete(`${ENDPOINT}/tags/${encodeURIComponent(name)}`, {});
|
||||
}
|
||||
|
||||
|
||||
@@ -83,10 +166,11 @@ export class API {
|
||||
return await API.post(`${ENDPOINT}/posts`, {
|
||||
title, description, tags, filename,
|
||||
fileMimeType, fileSize
|
||||
}, { Authorization: `Bearer ${user.accessToken}`})
|
||||
});
|
||||
}
|
||||
|
||||
public static async patchFileUploadUrl(uploadUrl: string, image: File, user: User): Promise<Response> {
|
||||
// This request is special and doesn't use the generic helper
|
||||
return await fetch(uploadUrl, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
@@ -99,11 +183,11 @@ export class API {
|
||||
}
|
||||
|
||||
public static async updatePost(postId: number, title: string, description: string, tags: string[], user: User): Promise<Response> {
|
||||
return await API.patch(`${ENDPOINT}/posts/${postId}`, { title, description, tags }, {Authorization: `Bearer ${user.accessToken}`});
|
||||
return await API.patch(`${ENDPOINT}/posts/${postId}`, { title, description, tags });
|
||||
}
|
||||
|
||||
public static async deletePost(postId: number, user: User): Promise<Response> {
|
||||
return await API.delete(`${ENDPOINT}/posts/${postId}`, {}, {Authorization: `Bearer ${user.accessToken}`});
|
||||
return await API.delete(`${ENDPOINT}/posts/${postId}`, {});
|
||||
}
|
||||
|
||||
/* Comment APIs */
|
||||
@@ -113,15 +197,15 @@ export class API {
|
||||
}
|
||||
|
||||
public static async postComment(postId: number, text: string, user: User): Promise<Response> {
|
||||
return await API.post(`${ENDPOINT}/posts/${postId}/comments`, { text }, {Authorization: `Bearer ${user.accessToken}`});
|
||||
return await API.post(`${ENDPOINT}/posts/${postId}/comments`, { text });
|
||||
}
|
||||
|
||||
public static async updateComment(postId: number, commentId: number, newText: string, user: User): Promise<Response> {
|
||||
return await API.patch(`${ENDPOINT}/posts/${postId}/comments/${commentId}`, { text: newText }, {Authorization: `Bearer ${user.accessToken}`});
|
||||
return await API.patch(`${ENDPOINT}/posts/${postId}/comments/${commentId}`, { text: newText });
|
||||
}
|
||||
|
||||
public static async deleteComment(postId: number, commentId: number, user: User): Promise<Response> {
|
||||
return await API.delete(`${ENDPOINT}/posts/${postId}/comments/${commentId}`, {}, {Authorization: `Bearer ${user.accessToken}`});
|
||||
return await API.delete(`${ENDPOINT}/posts/${postId}/comments/${commentId}`, {});
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user