import { redirect } from "@remix-run/node"; import { isAccessTokenExpired, isRefreshTokenExpired, sessionStorage, } from "~/sessions.server"; import { ApiApi, Configuration } from "../../src/api/types"; import { getInternalApiUrl } from "~/utils/api-config"; export interface User { access_token: string; refresh_token: string; access_token_timestamp: number; refresh_token_timestamp: number; } /** * Token manager for handling access and refresh tokens * - access_token expires after 1 week * - refresh_token expires after 1 month */ const protectedRoutes = [ "/profile/bookmarks", "/profile/following", "/profile/threads", "/profile/wallet", "/profile/location", "/profile/location/add", "/profile/location/edit", "/profile/orders", "/profile/setting", "/cart", "/store", "/admin", ]; export async function validateTokens(request: Request) { const session = await sessionStorage.getSession( request.headers.get("cookie"), ); const user: User | null = session.get("user"); const isProtectedRoute = protectedRoutes.some((route) => request.url.includes(route), ); // If no user data found, redirect to login if (!user || !user.access_token) { if (isProtectedRoute) { throw redirect("/login", { headers: { "Set-Cookie": await sessionStorage.destroySession(session), }, }); } else return { user: null }; } // Check if refresh_token is expired if ( user && user.refresh_token_timestamp && isRefreshTokenExpired(user.refresh_token_timestamp) ) { // If refresh token is expired, clear session and redirect to login if (isProtectedRoute) { throw redirect("/login", { headers: { "Set-Cookie": await sessionStorage.destroySession(session), }, }); } else return { user: null }; } // Check if access_token is expired but refresh_token is still valid if ( user && user.access_token_timestamp && isAccessTokenExpired(user.access_token_timestamp) ) { try { // Try to refresh the access token using the refresh token const newTokens = await refreshAccessToken(user.refresh_token); // Update session with new tokens and new timestamps const currentTime = Date.now(); session.set("user", { ...user, access_token: newTokens.access, refresh_token: newTokens.refresh, access_token_timestamp: currentTime, refresh_token_timestamp: currentTime, }); // Return the updated session and new tokens return { user: session.get("user"), headers: { "Set-Cookie": await sessionStorage.commitSession(session, { expires: new Date(new Date().setMonth(new Date().getMonth() + 1)), }), }, }; } catch (error) { // If refresh fails, redirect to login console.error("Token refresh failed:", error); if (isProtectedRoute) { throw redirect("/login", { headers: { "Set-Cookie": await sessionStorage.destroySession(session), }, }); } else return { user: null }; } } if (request.url.includes("/login")) throw redirect("/"); // If tokens are valid, return user data return { user }; } /** * Function to refresh the access token using the refresh token * This calls the API endpoint that handles token refresh */ async function refreshAccessToken(refreshToken: string) { try { const basePath = getInternalApiUrl(); const config = new Configuration({ basePath, headers: { "Content-Type": "application/json", accept: "application/json", }, }); const apiClient = new ApiApi(config); const response = await apiClient.apiUserV1TokenRefreshCreate({ data: { refresh: refreshToken, }, }); // Validate the response data if (!response.access || !response.refresh) { throw new Error("Invalid refresh token response"); } return response; } catch (error) { console.error("Error refreshing token:", error); throw error; } }