Created
January 26, 2026 01:11
-
-
Save feelchi1star/3211f2c36cab979aba2fc5f5a60760f1 to your computer and use it in GitHub Desktop.
Professional Axios and SSR authentication in React Router
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ============================================ | |
| // API Setup (axios-config.js) | |
| // ============================================ | |
| import axios from 'axios'; | |
| import { redirect } from 'react-router-dom'; | |
| const api = axios.create({ | |
| baseURL: 'https://api.something.vercel.app', | |
| withCredentials: true, | |
| }); | |
| // Handle 401 and refresh token | |
| api.interceptors.response.use( | |
| (response) => response, | |
| async (error) => { | |
| const originalRequest = error.config; | |
| if (error.response?.status === 401 && !originalRequest._retry) { | |
| originalRequest._retry = true; | |
| try { | |
| await api.post('/auth/refresh'); | |
| return api(originalRequest); | |
| } catch (refreshError) { | |
| localStorage.removeItem('isLogin'); | |
| window.location.href = '/login'; | |
| return Promise.reject(refreshError); | |
| } | |
| } | |
| return Promise.reject(error); | |
| } | |
| ); | |
| export default api; | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ============================================ | |
| // Server-Side Loader - Forwarding Cookies | |
| // ============================================ | |
| import type { Route } from "./+types/dashboard"; | |
| // Helper to get auth status (server-side) | |
| async function getAuthStatus(request: Request) { | |
| // Get cookies from the incoming browser request | |
| const cookieHeader = request.headers.get('Cookie'); | |
| try { | |
| const response = await fetch('https://api.something.vercel.app/auth/status', { | |
| headers: { | |
| 'Cookie': cookieHeader || '', // Forward cookies to API | |
| }, | |
| credentials: 'include', | |
| }); | |
| const data = await response.json(); | |
| return data; | |
| } catch (error) { | |
| console.error('Auth status check failed:', error); | |
| return { isLogin: false, permissions: [] }; | |
| } | |
| } | |
| // Dashboard loader with auth check | |
| export async function loader({ request, params }: Route.LoaderArgs) { | |
| // Get auth status by forwarding cookies | |
| const auth = await getAuthStatus(request); | |
| // Not logged in - redirect to login | |
| if (!auth.isLogin) { | |
| throw redirect('/login'); | |
| } | |
| // Check permissions | |
| if (!auth.permissions.includes('view_dashboard')) { | |
| throw redirect('/unauthorized'); | |
| } | |
| // Fetch dashboard data (forward cookies again) | |
| const cookieHeader = request.headers.get('Cookie'); | |
| const dashboardData = await fetch('https://api.something.vercel.app/dashboard/stats', { | |
| headers: { | |
| 'Cookie': cookieHeader || '', | |
| }, | |
| credentials: 'include', | |
| }); | |
| const stats = await dashboardData.json(); | |
| return { | |
| auth, | |
| stats, | |
| }; | |
| } | |
| // ============================================ | |
| // Reusable Auth Middleware Pattern | |
| // ============================================ | |
| import { redirect } from 'react-router'; | |
| interface AuthData { | |
| isLogin: boolean; | |
| permissions: string[]; | |
| role?: string; | |
| user?: { | |
| id: string; | |
| email: string; | |
| name: string; | |
| }; | |
| } | |
| // Reusable function to check auth | |
| export async function requireAuth( | |
| request: Request, | |
| requiredPermissions: string[] = [] | |
| ): Promise<AuthData> { | |
| const cookieHeader = request.headers.get('Cookie'); | |
| const response = await fetch('https://api.something.vercel.app/auth/status', { | |
| headers: { | |
| 'Cookie': cookieHeader || '', | |
| }, | |
| credentials: 'include', | |
| }); | |
| const auth: AuthData = await response.json(); | |
| // Not logged in | |
| if (!auth.isLogin) { | |
| throw redirect('/login'); | |
| } | |
| // Check permissions | |
| if (requiredPermissions.length > 0) { | |
| const hasPermission = requiredPermissions.some(perm => | |
| auth.permissions.includes(perm) | |
| ); | |
| if (!hasPermission) { | |
| throw redirect('/unauthorized'); | |
| } | |
| } | |
| return auth; | |
| } | |
| // Helper to make authenticated API calls | |
| export async function authenticatedFetch( | |
| request: Request, | |
| url: string, | |
| options: RequestInit = {} | |
| ) { | |
| const cookieHeader = request.headers.get('Cookie'); | |
| return fetch(url, { | |
| ...options, | |
| headers: { | |
| ...options.headers, | |
| 'Cookie': cookieHeader || '', | |
| }, | |
| credentials: 'include', | |
| }); | |
| } | |
| // ============================================ | |
| // Example Loaders Using Helpers | |
| // ============================================ | |
| // Dashboard loader | |
| export async function dashboardLoader({ request }: Route.LoaderArgs) { | |
| // Check auth with required permissions | |
| const auth = await requireAuth(request, ['view_dashboard']); | |
| // Fetch dashboard data | |
| const response = await authenticatedFetch( | |
| request, | |
| 'https://api.something.vercel.app/dashboard/stats' | |
| ); | |
| const stats = await response.json(); | |
| return { auth, stats }; | |
| } | |
| // Profile loader (no special permissions needed) | |
| export async function profileLoader({ request }: Route.LoaderArgs) { | |
| const auth = await requireAuth(request); | |
| const response = await authenticatedFetch( | |
| request, | |
| 'https://api.something.vercel.app/user/profile' | |
| ); | |
| const profile = await response.json(); | |
| return { auth, profile }; | |
| } | |
| // Admin loader | |
| export async function adminLoader({ request }: Route.LoaderArgs) { | |
| const auth = await requireAuth(request, ['admin_access']); | |
| const response = await authenticatedFetch( | |
| request, | |
| 'https://api.something.vercel.app/admin/users' | |
| ); | |
| const users = await response.json(); | |
| return { auth, users }; | |
| } | |
| // Public loader (check auth but don't require it) | |
| export async function homeLoader({ request }: Route.LoaderArgs) { | |
| const cookieHeader = request.headers.get('Cookie'); | |
| let auth: AuthData = { isLogin: false, permissions: [] }; | |
| try { | |
| const response = await fetch('https://api.something.vercel.app/auth/status', { | |
| headers: { 'Cookie': cookieHeader || '' }, | |
| credentials: 'include', | |
| }); | |
| auth = await response.json(); | |
| } catch (error) { | |
| // Not logged in is fine for public pages | |
| } | |
| return { auth }; | |
| } | |
| // Login loader (redirect if already logged in) | |
| export async function loginLoader({ request }: Route.LoaderArgs) { | |
| const cookieHeader = request.headers.get('Cookie'); | |
| try { | |
| const response = await fetch('https://api.something.vercel.app/auth/status', { | |
| headers: { 'Cookie': cookieHeader || '' }, | |
| credentials: 'include', | |
| }); | |
| const auth = await response.json(); | |
| if (auth.isLogin) { | |
| throw redirect('/dashboard'); | |
| } | |
| } catch (error) { | |
| // Not logged in, continue to login page | |
| } | |
| return null; | |
| } | |
| // ============================================ | |
| // Product Loader Example (Your Use Case) | |
| // ============================================ | |
| export async function productLoader({ request, params }: Route.LoaderArgs) { | |
| // Check if user is logged in (forward cookies) | |
| const auth = await requireAuth(request); | |
| // Fetch product with authentication | |
| const response = await authenticatedFetch( | |
| request, | |
| `https://api.something.vercel.app/products/${params.pid}` | |
| ); | |
| if (!response.ok) { | |
| throw new Response('Product not found', { status: 404 }); | |
| } | |
| const product = await response.json(); | |
| return { auth, product }; | |
| } | |
| // ============================================ | |
| // Backend Requirements (api.something.vercel.app) | |
| // ============================================ | |
| // Your backend MUST accept forwarded cookies | |
| import express from 'express'; | |
| import cookieParser from 'cookie-parser'; | |
| import jwt from 'jsonwebtoken'; | |
| const app = express(); | |
| app.use(cookieParser()); | |
| // Auth status endpoint | |
| app.get('/auth/status', async (req, res) => { | |
| // Cookies are automatically parsed from the Cookie header | |
| const { accessToken, refreshToken } = req.cookies; | |
| if (!accessToken && !refreshToken) { | |
| return res.json({ isLogin: false, permissions: [] }); | |
| } | |
| try { | |
| const decoded = jwt.verify(accessToken, process.env.ACCESS_TOKEN_SECRET); | |
| const user = await getUserById(decoded.userId); | |
| return res.json({ | |
| isLogin: true, | |
| permissions: user.permissions, | |
| role: user.role, | |
| user: { id: user.id, email: user.email, name: user.name }, | |
| }); | |
| } catch (error) { | |
| // Handle refresh token... | |
| return res.json({ isLogin: false, permissions: [] }); | |
| } | |
| }); | |
| // ============================================ | |
| // IMPORTANT: CORS Configuration for SSR | |
| // ============================================ | |
| // Your API needs to allow requests from your React Router server | |
| import cors from 'cors'; | |
| app.use(cors({ | |
| // Allow your React Router server domain | |
| origin: [ | |
| 'https://love.vercel.app', // Production | |
| 'http://localhost:3000', // Local dev | |
| ], | |
| credentials: true, // Important! | |
| })); | |
| // ============================================ | |
| // Complete Flow Diagram | |
| // ============================================ | |
| /* | |
| 1. Browser → React Router Server (love.vercel.app) | |
| Request: GET /dashboard | |
| Headers: Cookie: accessToken=abc123; refreshToken=xyz789 | |
| 2. React Router Server → API Server (api.something.vercel.app) | |
| Request: GET /auth/status | |
| Headers: Cookie: accessToken=abc123; refreshToken=xyz789 | |
| 3. API Server → React Router Server | |
| Response: { isLogin: true, permissions: [...] } | |
| 4. React Router Server → API Server | |
| Request: GET /dashboard/stats | |
| Headers: Cookie: accessToken=abc123; refreshToken=xyz789 | |
| 5. API Server → React Router Server | |
| Response: { stats: {...} } | |
| 6. React Router Server → Browser | |
| Response: Rendered HTML with data | |
| */ | |
| // ============================================ | |
| // Key Points | |
| // ============================================ | |
| /* | |
| ✅ Use request.headers.get('Cookie') to get cookies from browser | |
| ✅ Forward Cookie header to your API in fetch() calls | |
| ✅ Your API backend receives cookies normally (req.cookies) | |
| ✅ No special cookie handling needed - just forward the header | |
| ✅ Works with httpOnly cookies (most secure) | |
| ✅ Each loader can check auth independently | |
| ✅ Server-to-server communication is secure | |
| SECURITY: ✅✅✅✅✅ | |
| - Cookies never exposed to client JavaScript | |
| - Server forwards cookies securely | |
| - httpOnly cookies protected from XSS | |
| - Same security as traditional server-side apps | |
| */ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment