import type { User } from "~/context/AuthContext"; import type { Comment } from "~/types/comment"; import type { PaginatedResponse } from "~/types/PaginatedResponse"; import type { Post } from "~/types/post"; 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): Promise => { 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 { // 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 = {}): Promise { const localHeaders: Record = { 'Content-Type': 'application/json', ...headers, }; const options: RequestInit = { method, headers: localHeaders, }; if (body) { options.body = JSON.stringify(body); } return API._request(url, options); } private static async get(url: string, headers: Record = {}) { const response = await API._request(url, { headers }); if (!response.ok) { const errorData = await response.json().catch(() => ({ detail: `HTTP error! status: ${response.status}` })); throw { ...errorData, status: response.status }; } return response.json(); } public static async post(url: string, body: any, headers: Record = {}): Promise { return await API.genericRequest(url, 'POST', body, headers); } private static async patch(url: string, body: any, headers: Record = {}): Promise { return await API.genericRequest(url, 'PATCH', body, headers); } private static async delete(url: string, body: any, headers: Record = {}): Promise { return await API.genericRequest(url, 'DELETE', body, headers); } /* Tag APIs */ public static async fetchTags(page: number): Promise> { const data = await API.get(`${ENDPOINT}/tags?page=${page}`); return data } public static async createTag(name: string, tagType: string, user: User): Promise { return await API.post(`${ENDPOINT}/tags`, { name, type: tagType }); } public static async updateTag(name: string, newType: string, user: User): Promise { return await API.patch(`${ENDPOINT}/tags/${encodeURIComponent(name)}`, { type: newType }); } public static async deleteTag(name: string, user: User): Promise { return await API.delete(`${ENDPOINT}/tags/${encodeURIComponent(name)}`, {}); } /* Post APIs */ public static async fetchPostById(id: number): Promise { const data = await API.get(`${ENDPOINT}/posts/${id}`); return data; } public static async fetchPosts(page: number, filter: string = ""): Promise> { const data = await API.get(`${ENDPOINT}/posts?page=${page}&query=${encodeURIComponent(filter)}`); return data } public static async createPostRequest( title: string, description: string, tags: string[], filename: string, fileMimeType: string, fileSize: number, user: User ): Promise { return await API.post(`${ENDPOINT}/posts`, { title, description, tags, filename, fileMimeType, fileSize }); } public static async patchFileUploadUrl(uploadUrl: string, image: File, user: User): Promise { // This request is special and doesn't use the generic helper return await fetch(uploadUrl, { method: 'PATCH', headers: { 'Content-Range': `bytes 0-${image.size - 1}/${image.size}`, 'Content-Type': 'application/octet-stream', 'Authorization': `Bearer ${user.accessToken}`, }, body: image, }); } public static async updatePost(postId: number, title: string, description: string, tags: string[], user: User): Promise { return await API.patch(`${ENDPOINT}/posts/${postId}`, { title, description, tags }); } public static async deletePost(postId: number, user: User): Promise { return await API.delete(`${ENDPOINT}/posts/${postId}`, {}); } /* Comment APIs */ public static async fetchCommentsByPostId(postId: number, page: number): Promise> { const data = await API.get(`${ENDPOINT}/posts/${postId}/comments?page=${page}`); return data; } public static async postComment(postId: number, text: string, user: User): Promise { return await API.post(`${ENDPOINT}/posts/${postId}/comments`, { text }); } public static async updateComment(postId: number, commentId: number, newText: string, user: User): Promise { return await API.patch(`${ENDPOINT}/posts/${postId}/comments/${commentId}`, { text: newText }); } public static async deleteComment(postId: number, commentId: number, user: User): Promise { return await API.delete(`${ENDPOINT}/posts/${postId}/comments/${commentId}`, {}); } /* Auth APIs */ public static async register(username: string, email: string, password: string): Promise { return await API.post(`${ENDPOINT}/auth/register`, { username, email, password }); } public static async login(email: string, password: string): Promise { return await API.post(`${ENDPOINT}/auth/login`, { email, password }); } public static async renewToken(refreshToken: string): Promise { return await API.post(`${ENDPOINT}/auth/renew`, { refreshToken }); } public static async revokeToken(refreshToken: string): Promise { return await API.post(`${ENDPOINT}/auth/revoke`, { refreshToken }); } }