Skip to content

Commit fd7adb0

Browse files
authored
Merge pull request #1292 from trycompai/claudio/fix-portal
[dev] [claudfuen] claudio/fix-portal
2 parents f85027f + 1da0db8 commit fd7adb0

File tree

4 files changed

+231
-9
lines changed

4 files changed

+231
-9
lines changed

apps/app/src/app/layout.tsx

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,10 +89,6 @@ export default async function Layout({ children }: { children: React.ReactNode }
8989
return (
9090
<html lang="en" suppressHydrationWarning>
9191
<head>
92-
<link rel="icon" href="/favicon.ico" sizes="any" />
93-
<link rel="icon" href="/favicon-96x96.png" type="image/png" sizes="96x96" />
94-
<link rel="apple-touch-icon" href="/apple-touch-icon.png" sizes="180x180" />
95-
<link rel="manifest" href="/site.webmanifest" />
9692
{dubIsEnabled && dubReferUrl && (
9793
<DubAnalytics
9894
domainsConfig={{

apps/app/src/lib/api-client.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client';
22

33
import { env } from '@/env.mjs';
4-
import { getJwtToken } from '@/utils/auth-client';
4+
import { jwtManager } from '@/utils/jwt-manager';
55

66
interface ApiCallOptions extends Omit<RequestInit, 'headers'> {
77
organizationId?: string;
@@ -46,10 +46,12 @@ export class ApiClient {
4646
// Add JWT token for authentication
4747
if (typeof window !== 'undefined') {
4848
try {
49-
const token = await getJwtToken();
49+
// Get a valid (non-stale) JWT token
50+
const token = await jwtManager.getValidToken();
51+
5052
if (token) {
5153
headers['Authorization'] = `Bearer ${token}`;
52-
console.log('🎯 Using JWT token for API authentication');
54+
console.log('🎯 Using fresh JWT token for API authentication');
5355
} else {
5456
console.log('⚠️ No JWT token available for API authentication');
5557
}

apps/app/src/utils/auth-client.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,16 @@ export const authClient = createAuthClient({
2525
],
2626
fetchOptions: {
2727
onSuccess: (ctx) => {
28+
// JWT tokens are now managed by jwtManager for better expiry handling
29+
// Just log that we received tokens - jwtManager will handle storage
2830
const authToken = ctx.response.headers.get('set-auth-token');
2931
if (authToken) {
30-
localStorage.setItem('bearer_token', authToken);
31-
console.log('🎯 Bearer token captured and stored');
32+
console.log('🎯 Bearer token available in response');
33+
}
34+
35+
const jwtToken = ctx.response.headers.get('set-auth-jwt');
36+
if (jwtToken) {
37+
console.log('🎯 JWT token available in response');
3238
}
3339
},
3440
auth: {

apps/app/src/utils/jwt-manager.ts

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
'use client';
2+
3+
import { authClient } from './auth-client';
4+
5+
interface TokenInfo {
6+
token: string;
7+
expiresAt: number; // Unix timestamp
8+
}
9+
10+
class JWTManager {
11+
private refreshTimer: NodeJS.Timeout | null = null;
12+
private readonly REFRESH_THRESHOLD = 5 * 60 * 1000; // Refresh 5 minutes before expiry
13+
private readonly STORAGE_KEY = 'bearer_token';
14+
private readonly EXPIRY_KEY = 'bearer_token_expiry';
15+
16+
/**
17+
* Get the current JWT token, refreshing if needed
18+
*/
19+
async getValidToken(): Promise<string | null> {
20+
try {
21+
const stored = this.getStoredToken();
22+
23+
// If no token or expired, get a fresh one
24+
if (!stored || this.isTokenExpiringSoon(stored.expiresAt)) {
25+
console.log('🔄 JWT token missing or expiring soon, fetching fresh token...');
26+
return await this.refreshToken();
27+
}
28+
29+
console.log('✅ Using cached JWT token');
30+
return stored.token;
31+
} catch (error) {
32+
console.error('❌ Error getting valid JWT token:', error);
33+
return null;
34+
}
35+
}
36+
37+
/**
38+
* Get a fresh JWT token from Better Auth
39+
*/
40+
async refreshToken(): Promise<string | null> {
41+
try {
42+
console.log('🔄 Refreshing JWT token...');
43+
44+
let newToken: string | null = null;
45+
46+
// Try to get JWT from session call
47+
const sessionResponse = await authClient.getSession({
48+
fetchOptions: {
49+
onSuccess: (ctx) => {
50+
const jwtToken = ctx.response.headers.get('set-auth-jwt');
51+
if (jwtToken) {
52+
newToken = jwtToken;
53+
console.log('✅ JWT token refreshed via session');
54+
}
55+
},
56+
},
57+
});
58+
59+
// If that didn't work, try the explicit token endpoint
60+
if (!newToken) {
61+
try {
62+
const tokenResponse = await fetch('/api/auth/token', {
63+
credentials: 'include',
64+
});
65+
66+
if (tokenResponse.ok) {
67+
const tokenData = await tokenResponse.json();
68+
newToken = tokenData.token;
69+
console.log('✅ JWT token refreshed via token endpoint');
70+
}
71+
} catch (error) {
72+
console.warn('⚠️ Token endpoint failed, using session token');
73+
}
74+
}
75+
76+
if (newToken) {
77+
this.storeToken(newToken);
78+
this.scheduleRefresh(newToken);
79+
return newToken;
80+
}
81+
82+
console.error('❌ Failed to refresh JWT token');
83+
return null;
84+
} catch (error) {
85+
console.error('❌ Error refreshing JWT token:', error);
86+
return null;
87+
}
88+
}
89+
90+
/**
91+
* Store token with expiry information
92+
*/
93+
private storeToken(token: string): void {
94+
try {
95+
const payload = this.decodeJWTPayload(token);
96+
const expiresAt = payload.exp * 1000; // Convert to milliseconds
97+
98+
localStorage.setItem(this.STORAGE_KEY, token);
99+
localStorage.setItem(this.EXPIRY_KEY, expiresAt.toString());
100+
101+
console.log(`🔐 JWT token stored, expires at: ${new Date(expiresAt).toISOString()}`);
102+
} catch (error) {
103+
console.error('❌ Error storing JWT token:', error);
104+
}
105+
}
106+
107+
/**
108+
* Get stored token with expiry info
109+
*/
110+
private getStoredToken(): TokenInfo | null {
111+
try {
112+
const token = localStorage.getItem(this.STORAGE_KEY);
113+
const expiryStr = localStorage.getItem(this.EXPIRY_KEY);
114+
115+
if (!token || !expiryStr) return null;
116+
117+
return {
118+
token,
119+
expiresAt: parseInt(expiryStr, 10),
120+
};
121+
} catch (error) {
122+
console.error('❌ Error getting stored token:', error);
123+
return null;
124+
}
125+
}
126+
127+
/**
128+
* Check if token is expiring soon
129+
*/
130+
private isTokenExpiringSoon(expiresAt: number): boolean {
131+
const now = Date.now();
132+
const timeUntilExpiry = expiresAt - now;
133+
return timeUntilExpiry <= this.REFRESH_THRESHOLD;
134+
}
135+
136+
/**
137+
* Decode JWT payload without verification (client-side only)
138+
*/
139+
private decodeJWTPayload(token: string): any {
140+
try {
141+
const parts = token.split('.');
142+
const payload = JSON.parse(atob(parts[1]));
143+
return payload;
144+
} catch (error) {
145+
throw new Error('Invalid JWT token format');
146+
}
147+
}
148+
149+
/**
150+
* Schedule automatic token refresh
151+
*/
152+
private scheduleRefresh(token: string): void {
153+
try {
154+
// Clear existing timer
155+
if (this.refreshTimer) {
156+
clearTimeout(this.refreshTimer);
157+
}
158+
159+
const payload = this.decodeJWTPayload(token);
160+
const expiresAt = payload.exp * 1000;
161+
const now = Date.now();
162+
const refreshIn = Math.max(0, expiresAt - now - this.REFRESH_THRESHOLD);
163+
164+
console.log(`⏰ Scheduling JWT refresh in ${Math.round(refreshIn / 1000 / 60)} minutes`);
165+
166+
this.refreshTimer = setTimeout(async () => {
167+
console.log('⏰ Auto-refreshing JWT token...');
168+
await this.refreshToken();
169+
}, refreshIn);
170+
} catch (error) {
171+
console.error('❌ Error scheduling token refresh:', error);
172+
}
173+
}
174+
175+
/**
176+
* Initialize the JWT manager (call this once on app start)
177+
*/
178+
async initialize(): Promise<void> {
179+
console.log('🚀 Initializing JWT Manager...');
180+
181+
// Get a valid token and set up refresh schedule
182+
const token = await this.getValidToken();
183+
184+
if (token) {
185+
this.scheduleRefresh(token);
186+
}
187+
}
188+
189+
/**
190+
* Clean up timers and storage
191+
*/
192+
cleanup(): void {
193+
if (this.refreshTimer) {
194+
clearTimeout(this.refreshTimer);
195+
this.refreshTimer = null;
196+
}
197+
198+
localStorage.removeItem(this.STORAGE_KEY);
199+
localStorage.removeItem(this.EXPIRY_KEY);
200+
console.log('🧹 JWT Manager cleaned up');
201+
}
202+
203+
/**
204+
* Force refresh token (useful for testing or manual refresh)
205+
*/
206+
async forceRefresh(): Promise<string | null> {
207+
console.log('🔄 Force refreshing JWT token...');
208+
return await this.refreshToken();
209+
}
210+
}
211+
212+
// Export a singleton instance
213+
export const jwtManager = new JWTManager();
214+
215+
// Auto-initialize when imported
216+
if (typeof window !== 'undefined') {
217+
jwtManager.initialize();
218+
}

0 commit comments

Comments
 (0)