diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts new file mode 100644 index 0000000..2388746 --- /dev/null +++ b/frontend/src/services/api.ts @@ -0,0 +1,114 @@ +import axios from "axios"; +import { refreshAccessToken, getAccessToken, logout, API_URL } from "./auth"; + +const apiInstance = axios.create({ + baseURL: API_URL, + headers: { + "Content-Type": "application/json", + }, +}); + +// Request Interceptor - Attaches access token +apiInstance.interceptors.request.use( + async (config) => { + const token = getAccessToken(); + if (token) { + config.headers["Authorization"] = `Bearer ${token}`; + } + return config; + }, + (error) => Promise.reject(error) +); + +// Response Interceptor - Refreshes token if needed +apiInstance.interceptors.response.use( + (response) => response, + async (error) => { + const originalRequest = error.config; + + if (error.response) { + // Check for 401 Unauthorized + if (error.response.status === 401) { + console.log("401 Unauthorized detected."); + } + + // Check for 403 with token invalid message + if (error.response.status === 403 && error.response.data?.code === "token_not_valid") { + console.log("403 Forbidden detected with 'token_not_valid'."); + } + + // Handle both cases + if ((error.response.status === 401 || error.response.status === 403) && !originalRequest._retry) { + console.log("Attempting token refresh..."); + + originalRequest._retry = true; + + const newAccessToken = await refreshAccessToken(); + if (newAccessToken) { + originalRequest.headers["Authorization"] = `Bearer ${newAccessToken}`; + return apiInstance(originalRequest); + } else { + console.log("Token refresh failed, logging out user."); + } + } + } + + return Promise.reject(error); + } +); + +// API Request Functions +// ------------------- + +// Fetch posts example +export const fetchPosts = async (page: number = 1) => { + const response = await apiInstance.get(`/posts/?page=${page}&page_size=25`); + return response.data; +}; + +// Fetch a specific post by source site, creator, and ID +export const fetchPostByDetails = async (source_site: string, creator: string, id: string) => { + const response = await apiInstance.get(`/posts/${source_site}/${creator}/${id}/`); + return response.data; +}; + +// Function to get file URL with authentication +export const getFileUrl = (fileHash: string, options?: { thumbnail?: 'sx' | 'sm' | 'md' | 'lg' | 'xl', download?: boolean }) => { + const token = getAccessToken(); + + // Build URL with query parameters + let url = `${API_URL}/files/${fileHash}/`; + const queryParams: string[] = []; + + if (options?.thumbnail) { + queryParams.push(`t=${options.thumbnail}`); + } + + if (options?.download !== undefined) { + queryParams.push(`d=${options.download ? '0' : '1'}`); + } + + if (queryParams.length > 0) { + url += `?${queryParams.join('&')}`; + } + + // Append token as query parameter + url += (queryParams.length > 0 ? '&' : '?') + `token=${token}`; + + return url; +}; + +// Direct fetch function if you need the file data instead of a URL +export const fetchFile = async (fileHash: string, options?: { thumbnail?: 'sx' | 'sm' | 'md' | 'lg' | 'xl', download?: boolean }) => { + const response = await apiInstance.get(`${API_URL}/files/${fileHash}/`, { + params: { + ...(options?.thumbnail && { t: options.thumbnail }), + ...(options?.download !== undefined && { d: options.download ? '0' : '1' }), + }, + responseType: 'blob', // Important for handling binary data + }); + + return response.data; +}; + +export default apiInstance; diff --git a/frontend/src/services/auth.ts b/frontend/src/services/auth.ts index 9c5485f..0694097 100644 --- a/frontend/src/services/auth.ts +++ b/frontend/src/services/auth.ts @@ -2,15 +2,8 @@ import axios from "axios"; const API_URL = "/api"; -const apiInstance = axios.create({ - baseURL: API_URL, - headers: { - "Content-Type": "application/json", - }, -}); - // Helper function to refresh tokens -const refreshAccessToken = async () => { +export const refreshAccessToken = async () => { try { const refreshToken = localStorage.getItem("refresh_token"); if (!refreshToken) throw new Error("No refresh token available"); @@ -33,58 +26,9 @@ const refreshAccessToken = async () => { } }; -// Request Interceptor - Attaches access token -apiInstance.interceptors.request.use( - async (config) => { - const token = localStorage.getItem("access_token"); - if (token) { - config.headers["Authorization"] = `Bearer ${token}`; - } - return config; - }, - (error) => Promise.reject(error) -); - -// Response Interceptor - Refreshes token if needed -apiInstance.interceptors.response.use( - (response) => response, - async (error) => { - const originalRequest = error.config; - - if (error.response) { - // Check for 401 Unauthorized - if (error.response.status === 401) { - console.log("401 Unauthorized detected."); - } - - // Check for 403 with token invalid message - if (error.response.status === 403 && error.response.data?.code === "token_not_valid") { - console.log("403 Forbidden detected with 'token_not_valid'."); - } - - // Handle both cases - if ((error.response.status === 401 || error.response.status === 403) && !originalRequest._retry) { - console.log("Attempting token refresh..."); - - originalRequest._retry = true; - - const newAccessToken = await refreshAccessToken(); - if (newAccessToken) { - originalRequest.headers["Authorization"] = `Bearer ${newAccessToken}`; - return apiInstance(originalRequest); - } else { - console.log("Token refresh failed, logging out user."); - } - } - } - - return Promise.reject(error); - } -); - // Login function export const login = async (username: string, password: string) => { - const response = await apiInstance.post("/auth/token/", { username, password }); + const response = await axios.post(`${API_URL}/auth/token/`, { username, password }); localStorage.setItem("access_token", response.data.access); localStorage.setItem("refresh_token", response.data.refresh); return response.data; @@ -101,11 +45,10 @@ export const logout = () => { window.location.href = "/user/login"; // Redirect to login page }; -// Fetch posts example -export const fetchPosts = async () => { - const response = await apiInstance.get("/posts/"); - return response.data; +// Get stored access token +export const getAccessToken = () => { + return localStorage.getItem("access_token"); }; -export default apiInstance; - +// Export API_URL for consistency +export { API_URL };