This repository has no description
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}