This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

semble / src / webapp / app / api / auth / me / route.ts
6.0 kB 185 lines
1import { NextRequest, NextResponse } from 'next/server'; 2import type { GetProfileResponse } from '@/api-client/ApiClient'; 3import { cookies } from 'next/headers'; 4import { isTokenExpiringSoon } from '@/lib/auth/token'; 5 6const ENABLE_REFRESH_LOGGING = true; 7 8const backendUrl = 9 process.env.NEXT_PUBLIC_API_BASE_URL || 'http://127.0.0.1:3000'; 10 11type AuthResult = { 12 isAuth: boolean; 13 user?: GetProfileResponse; 14}; 15 16// Prevent concurrent refresh attempts 17let refreshPromise: Promise<Response> | null = null; 18 19export async function GET(request: NextRequest) { 20 try { 21 const cookieStore = await cookies(); 22 let accessToken = cookieStore.get('accessToken')?.value; 23 const refreshToken = cookieStore.get('refreshToken')?.value; 24 25 // No tokens at all - not authenticated 26 if (!accessToken && !refreshToken) { 27 return NextResponse.json<AuthResult>({ isAuth: false }, { status: 401 }); 28 } 29 30 // Check if accessToken is expired/missing or expiring soon 31 if ((!accessToken || isTokenExpiringSoon(accessToken)) && refreshToken) { 32 if (ENABLE_REFRESH_LOGGING) { 33 const tokenPreview = '...' + refreshToken.slice(-8); 34 console.log( 35 `[auth/me] Access token missing/expiring, attempting refresh with token: ${tokenPreview}`, 36 ); 37 } 38 39 // Use mutex to prevent concurrent refresh attempts 40 if (!refreshPromise) { 41 refreshPromise = performTokenRefresh(refreshToken, request); 42 } 43 44 try { 45 const result = await refreshPromise; 46 if (ENABLE_REFRESH_LOGGING) { 47 console.log(`[auth/me] Token refresh completed successfully`); 48 } 49 return result; 50 } catch (error: any) { 51 if (ENABLE_REFRESH_LOGGING) { 52 console.log(`[auth/me] Token refresh error: ${error}`); 53 } 54 console.error('Token refresh error:', error); 55 56 // If this is a refresh failure with backend response, forward the cookie-clearing headers 57 if (error.backendResponse) { 58 const response = NextResponse.json<AuthResult>( 59 { isAuth: false }, 60 { status: 500 }, 61 ); 62 63 // Forward the Set-Cookie headers from backend to clear cookies 64 const setCookieHeader = 65 error.backendResponse.headers.get('set-cookie'); 66 if (setCookieHeader) { 67 response.headers.set('Set-Cookie', setCookieHeader); 68 } 69 70 return response; 71 } 72 73 // For other errors, clear cookies manually 74 const response = NextResponse.json<AuthResult>( 75 { isAuth: false }, 76 { status: 500 }, 77 ); 78 response.cookies.delete('accessToken'); 79 response.cookies.delete('refreshToken'); 80 return response; 81 } finally { 82 refreshPromise = null; 83 } 84 } 85 86 // AccessToken is valid - fetch profile 87 try { 88 const backendUrl = 89 process.env.NEXT_PUBLIC_API_BASE_URL || 'http://127.0.0.1:3000'; 90 const profileResponse = await fetch(`${backendUrl}/api/users/me`, { 91 method: 'GET', 92 headers: { 93 'Content-Type': 'application/json', 94 Cookie: `accessToken=${accessToken}`, 95 }, 96 }); 97 98 if (!profileResponse.ok) { 99 // Clear cookies on auth failure 100 const response = NextResponse.json<AuthResult>( 101 { isAuth: false }, 102 { status: profileResponse.status }, 103 ); 104 response.cookies.delete('accessToken'); 105 response.cookies.delete('refreshToken'); 106 return response; 107 } 108 109 const user = await profileResponse.json(); 110 return NextResponse.json<AuthResult>({ isAuth: true, user }); 111 } catch (error) { 112 console.error('Profile fetch error:', error); 113 // Clear cookies on fetch error too 114 const response = NextResponse.json<AuthResult>( 115 { isAuth: false }, 116 { status: 500 }, 117 ); 118 response.cookies.delete('accessToken'); 119 response.cookies.delete('refreshToken'); 120 return response; 121 } 122 } catch (error) { 123 console.error('Auth me error:', error); 124 refreshPromise = null; // Reset on error 125 return NextResponse.json<AuthResult>({ isAuth: false }, { status: 500 }); 126 } 127} 128 129async function performTokenRefresh( 130 refreshToken: string, 131 request: NextRequest, 132): Promise<Response> { 133 if (ENABLE_REFRESH_LOGGING) { 134 console.log(`[auth/me] Sending refresh request to backend`); 135 } 136 137 // Proxy the refresh request completely to backend 138 const refreshResponse = await fetch(`${backendUrl}/api/users/oauth/refresh`, { 139 method: 'POST', 140 headers: { 141 'Content-Type': 'application/json', 142 Cookie: request.headers.get('cookie') || '', 143 }, 144 body: JSON.stringify({ refreshToken }), 145 }); 146 147 if (!refreshResponse.ok) { 148 if (ENABLE_REFRESH_LOGGING) { 149 console.log( 150 `[auth/me] Backend refresh failed with status: ${refreshResponse.status}. Message: ${await refreshResponse.text()}`, 151 ); 152 } 153 // Create error with backend response to preserve cookie-clearing headers 154 const error = new Error(`Refresh failed: ${refreshResponse.status}`) as any; 155 error.backendResponse = refreshResponse; 156 throw error; 157 } 158 159 // Get new tokens from response 160 const newTokens = await refreshResponse.json(); 161 const accessToken = newTokens.accessToken; 162 163 // Fetch profile with new token 164 const profileResponse = await fetch(`${backendUrl}/api/users/me`, { 165 method: 'GET', 166 headers: { 167 'Content-Type': 'application/json', 168 Cookie: `accessToken=${accessToken}`, 169 }, 170 }); 171 172 if (!profileResponse.ok) { 173 return NextResponse.json<AuthResult>({ isAuth: false }, { status: 401 }); 174 } 175 176 const user = await profileResponse.json(); 177 // Return user profile with backend's Set-Cookie headers 178 return new Response(JSON.stringify({ isAuth: true, user }), { 179 status: 200, 180 headers: { 181 'Content-Type': 'application/json', 182 'Set-Cookie': refreshResponse.headers.get('set-cookie') || '', 183 }, 184 }); 185}