Skip to content

Commit 86397e0

Browse files
committed
Fix auth 500: add DynamoDB fallback and graceful session error handling
- dynamodb.ts: add withFallback() wrapper that catches DynamoDB errors and transparently retries against in-memory adapter, sets dynamoFailed flag - session route: return 401 instead of 500 when validation fails, so the AuthContext handles it silently instead of showing error banners
1 parent fcc592c commit 86397e0

2 files changed

Lines changed: 34 additions & 23 deletions

File tree

app/api/auth/session/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,6 @@ export async function GET(request: NextRequest) {
3939
});
4040
} catch (err) {
4141
console.error("[auth/session] Error validating session:", err);
42-
return NextResponse.json({ user: null, authenticated: false }, { status: 500 });
42+
return NextResponse.json({ user: null, authenticated: false }, { status: 401 });
4343
}
4444
}

app/lib/auth/dynamodb.ts

Lines changed: 33 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export interface AuthSession {
3333
// On Amplify Lambda, credentials come via IAM role (SDK auto-detects).
3434
// Only fall back to in-memory when DynamoDB actually fails or in local dev
3535
// without any AWS config.
36-
const dynamoFailed = false;
36+
let dynamoFailed = false;
3737

3838
function shouldUseDynamo(): boolean {
3939
if (dynamoFailed) return false;
@@ -49,6 +49,27 @@ function shouldUseDynamo(): boolean {
4949
return true;
5050
}
5151

52+
/**
53+
* Try a DynamoDB operation; on failure, set dynamoFailed = true and
54+
* transparently retry against the in-memory adapter.
55+
*/
56+
async function withFallback<T>(
57+
operation: string,
58+
dynamoFn: () => Promise<T>,
59+
memoryFn: () => Promise<T>,
60+
): Promise<T> {
61+
if (!shouldUseDynamo()) {
62+
return memoryFn();
63+
}
64+
try {
65+
return await dynamoFn();
66+
} catch (err) {
67+
console.warn(`[auth/dynamodb] ${operation} failed, falling back to in-memory:`, err);
68+
dynamoFailed = true;
69+
return memoryFn();
70+
}
71+
}
72+
5273
// ─── In-memory fallback for local dev ────────────────────────────────
5374
const memoryUsers = new Map<string, AuthUser>();
5475
const memoryUsersByEmail = new Map<string, AuthUser>();
@@ -231,41 +252,34 @@ const dynamoAdapter = {
231252
},
232253
};
233254

234-
// ─── Exported interface — auto-selects adapter ───────────────────────
235-
function getAdapter() {
236-
if (shouldUseDynamo()) {
237-
return dynamoAdapter;
238-
}
239-
console.warn("[auth/dynamodb] Using in-memory store (DynamoDB unavailable)");
240-
return memoryAdapter;
241-
}
255+
// ─── Exported interface — uses withFallback for resilience ───────────
242256

243257
export async function createUser(user: AuthUser): Promise<AuthUser> {
244-
return getAdapter().createUser(user);
258+
return withFallback("createUser", () => dynamoAdapter.createUser(user), () => memoryAdapter.createUser(user));
245259
}
246260

247261
export async function getUserByEmail(email: string): Promise<AuthUser | null> {
248-
return getAdapter().getUserByEmail(email);
262+
return withFallback("getUserByEmail", () => dynamoAdapter.getUserByEmail(email), () => memoryAdapter.getUserByEmail(email));
249263
}
250264

251265
export async function getUserById(id: string): Promise<AuthUser | null> {
252-
return getAdapter().getUserById(id);
266+
return withFallback("getUserById", () => dynamoAdapter.getUserById(id), () => memoryAdapter.getUserById(id));
253267
}
254268

255269
export async function updateUser(id: string, updates: Partial<AuthUser>): Promise<AuthUser | null> {
256-
return getAdapter().updateUser(id, updates);
270+
return withFallback("updateUser", () => dynamoAdapter.updateUser(id, updates), () => memoryAdapter.updateUser(id, updates));
257271
}
258272

259273
export async function createAuthSession(session: AuthSession): Promise<AuthSession> {
260-
return getAdapter().createAuthSession(session);
274+
return withFallback("createAuthSession", () => dynamoAdapter.createAuthSession(session), () => memoryAdapter.createAuthSession(session));
261275
}
262276

263277
export async function getAuthSession(token: string): Promise<AuthSession | null> {
264-
return getAdapter().getAuthSession(token);
278+
return withFallback("getAuthSession", () => dynamoAdapter.getAuthSession(token), () => memoryAdapter.getAuthSession(token));
265279
}
266280

267281
export async function deleteAuthSession(token: string): Promise<void> {
268-
return getAdapter().deleteAuthSession(token);
282+
return withFallback("deleteAuthSession", () => dynamoAdapter.deleteAuthSession(token), () => memoryAdapter.deleteAuthSession(token));
269283
}
270284

271285
/**
@@ -278,20 +292,17 @@ export async function upsertGoogleUser(profile: {
278292
name: string;
279293
picture: string;
280294
}): Promise<AuthUser> {
281-
const adapter = getAdapter();
282-
const existing = await adapter.getUserByEmail(profile.email);
295+
const existing = await getUserByEmail(profile.email);
283296

284297
if (existing) {
285-
// Update name/picture/googleId if they changed
286-
const updated = await adapter.updateUser(existing.id, {
298+
const updated = await updateUser(existing.id, {
287299
name: profile.name,
288300
picture: profile.picture,
289301
googleId: profile.id,
290302
});
291303
return updated!;
292304
}
293305

294-
// New user
295306
const now = new Date().toISOString();
296307
const newUser: AuthUser = {
297308
id: crypto.randomUUID(),
@@ -302,7 +313,7 @@ export async function upsertGoogleUser(profile: {
302313
createdAt: now,
303314
updatedAt: now,
304315
};
305-
return adapter.createUser(newUser);
316+
return createUser(newUser);
306317
}
307318

308319
/**

0 commit comments

Comments
 (0)