Files
T120B165/Client/app/api/api.ts

231 lines
9.0 KiB
TypeScript

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<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: Record<string, string> = {
'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<string, string> = {}) {
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<string, string> = {}): Promise<Response> {
return await API.genericRequest(url, 'POST', body, headers);
}
private static async patch(url: string, body: any, headers: Record<string, string> = {}): Promise<Response> {
return await API.genericRequest(url, 'PATCH', body, headers);
}
private static async delete(url: string, body: any, headers: Record<string, string> = {}): Promise<Response> {
return await API.genericRequest(url, 'DELETE', body, headers);
}
/* Tag APIs */
public static async fetchTags(page: number): Promise<PaginatedResponse<Tag>> {
const data = await API.get(`${ENDPOINT}/tags?page=${page}`);
return data
}
public static async createTag(name: string, tagType: string, user: User): Promise<Response> {
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 });
}
public static async deleteTag(name: string, user: User): Promise<Response> {
return await API.delete(`${ENDPOINT}/tags/${encodeURIComponent(name)}`, {});
}
/* Post APIs */
public static async fetchPostById(id: number): Promise<Post|null> {
const data = await API.get(`${ENDPOINT}/posts/${id}`);
return data;
}
public static async fetchPosts(page: number, filter: string = ""): Promise<PaginatedResponse<Post>> {
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<Response> {
return await API.post(`${ENDPOINT}/posts`, {
title, description, tags, filename,
fileMimeType, fileSize
});
}
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: {
'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<Response> {
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}`, {});
}
/* Comment APIs */
public static async fetchCommentsByPostId(postId: number, page: number): Promise<PaginatedResponse<Comment>> {
const data = await API.get(`${ENDPOINT}/posts/${postId}/comments?page=${page}`);
return data;
}
public static async postComment(postId: number, text: string, user: User): Promise<Response> {
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 });
}
public static async deleteComment(postId: number, commentId: number, user: User): Promise<Response> {
return await API.delete(`${ENDPOINT}/posts/${postId}/comments/${commentId}`, {});
}
/* Auth APIs */
public static async register(username: string, email: string, password: string): Promise<Response> {
return await API.post(`${ENDPOINT}/auth/register`, { username, email, password });
}
public static async login(email: string, password: string): Promise<Response> {
return await API.post(`${ENDPOINT}/auth/login`, { email, password });
}
public static async renewToken(refreshToken: string): Promise<Response> {
return await API.post(`${ENDPOINT}/auth/renew`, { refreshToken });
}
public static async revokeToken(refreshToken: string): Promise<Response> {
return await API.post(`${ENDPOINT}/auth/revoke`, { refreshToken });
}
}