Skip to content

Commit 7237951

Browse files
committed
fix(auth): improve auth error handling and fix Link CORS issue (#107)
Why: The auth module had three issues: 1. <Link> to auth API routes caused unnecessary RSC fetch + CORS errors 2. getSession() coupled auth and DB access, making error types indistinguishable 3. authActionClient used a separate auth path (getCurrentUser) from getSession() What: - Split getSession() into getAuthSession(), tryGetAuthSession(), getSessionWithUser() - Add React cache() for per-request memoization - Replace <Link> with <a> for auth routes that redirect to Cognito - Unify authActionClient to use getAuthSession() - Convert Header.tsx from Client to Server Component
1 parent 764a4fa commit 7237951

File tree

6 files changed

+62
-61
lines changed

6 files changed

+62
-61
lines changed

webapp/src/app/(root)/page.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { prisma } from '@/lib/prisma';
2-
import { getSession } from '@/lib/auth';
2+
import { getAuthSession } from '@/lib/auth';
33
import TodoItemComponent from './components/TodoItem';
44
import CreateTodoForm from './components/CreateTodoForm';
55
import { TodoItemStatus } from '@prisma/client';
66
import Header from '@/components/Header';
77

88
export default async function Home() {
9-
const { userId } = await getSession();
9+
const { userId } = await getAuthSession();
1010

1111
const todos = await prisma.todoItem.findMany({
1212
where: {
Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,16 @@
11
import { redirect } from 'next/navigation';
2-
import { getSession, UserNotCreatedError } from '@/lib/auth';
2+
import { getAuthSession } from '@/lib/auth';
33
import { prisma } from '@/lib/prisma';
44

55
export const dynamic = 'force-dynamic';
66

77
export default async function AuthCallbackPage() {
8-
try {
9-
await getSession();
10-
} catch (e) {
11-
console.log(e);
12-
if (e instanceof UserNotCreatedError) {
13-
const userId = e.userId;
14-
console.log(userId);
15-
await prisma.user.create({
16-
data: {
17-
id: userId,
18-
},
19-
});
20-
} else {
21-
throw e;
22-
}
8+
const { userId } = await getAuthSession();
9+
10+
const user = await prisma.user.findUnique({ where: { id: userId } });
11+
if (user == null) {
12+
await prisma.user.create({ data: { id: userId } });
2313
}
14+
2415
redirect('/');
2516
}

webapp/src/app/sign-in/page.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import Link from 'next/link';
2-
31
export default function SignInPage() {
42
return (
53
<div className="min-h-screen bg-gray-50 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
@@ -15,15 +13,18 @@ export default function SignInPage() {
1513
Please sign in with your Cognito account to continue
1614
</p>
1715

18-
<Link
16+
{/* Use <a> instead of <Link> to trigger a full-page navigation.
17+
The sign-in route returns a 302 redirect to Cognito, which
18+
would cause a CORS error if fetched via client-side navigation. */}
19+
{/* eslint-disable-next-line @next/next/no-html-link-for-pages */}
20+
<a
1921
href="/api/auth/sign-in"
2022
// you can add a query string to change the locale of cognito managed login page.
2123
// href="/api/auth/sign-in?lang=ja"
2224
className="w-full flex justify-center py-3 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
23-
prefetch={false} // prevent CORS error
2425
>
2526
Sign in with Cognito
26-
</Link>
27+
</a>
2728
</div>
2829
</div>
2930
</div>

webapp/src/components/Header.tsx

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
1-
'use client';
2-
31
import Link from 'next/link';
4-
import { useRouter } from 'next/navigation';
52

63
export default function Header() {
7-
const router = useRouter();
8-
94
return (
105
<header className="bg-indigo-600 text-white shadow-md">
116
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
@@ -16,13 +11,16 @@ export default function Header() {
1611
</Link>
1712
</div>
1813
<div>
19-
<Link
14+
{/* Use <a> instead of <Link> to trigger a full-page navigation.
15+
The sign-out route returns a 302 redirect to Cognito, which
16+
would cause a CORS error if fetched via client-side navigation. */}
17+
{/* eslint-disable-next-line @next/next/no-html-link-for-pages */}
18+
<a
2019
href="/api/auth/sign-out"
2120
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-indigo-600 bg-white hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
22-
prefetch={false} // prevent CORS error
2321
>
2422
Sign Out
25-
</Link>
23+
</a>
2624
</div>
2725
</div>
2826
</div>

webapp/src/lib/auth.ts

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,58 @@
1+
import { cache } from 'react';
12
import { cookies } from 'next/headers';
23
import { fetchAuthSession } from 'aws-amplify/auth/server';
34
import { runWithAmplifyServerContext } from '@/lib/amplifyServerUtils';
45
import { prisma } from '@/lib/prisma';
56

6-
export class UserNotCreatedError {
7-
constructor(public readonly userId: string) {}
8-
}
9-
10-
export async function getSession() {
7+
/**
8+
* Get the authenticated session without DB access.
9+
* Use when only userId/email/accessToken is needed.
10+
* Memoized per request via React cache().
11+
*/
12+
export const getAuthSession = cache(async () => {
1113
const session = await runWithAmplifyServerContext({
1214
nextServerContext: { cookies },
1315
operation: (contextSpec) => fetchAuthSession(contextSpec),
1416
});
1517
if (session.userSub == null || session.tokens?.idToken == null || session.tokens?.accessToken == null) {
1618
throw new Error('session not found');
1719
}
18-
const userId = session.userSub;
1920
const email = session.tokens.idToken.payload.email;
2021
if (typeof email != 'string') {
21-
throw new Error(`invalid email ${userId}.`);
22-
}
23-
const user = await prisma.user.findUnique({
24-
where: {
25-
id: userId,
26-
},
27-
});
28-
if (user == null) {
29-
throw new UserNotCreatedError(userId);
22+
throw new Error(`invalid email ${session.userSub}.`);
3023
}
31-
3224
return {
33-
userId: user.id,
25+
userId: session.userSub,
3426
email,
3527
accessToken: session.tokens.accessToken.toString(),
36-
user,
3728
};
29+
});
30+
31+
/**
32+
* Try to get the authenticated session, returning null on failure.
33+
* Use in API Routes where you need to distinguish 401 from 500.
34+
*/
35+
export async function tryGetAuthSession() {
36+
try {
37+
return await getAuthSession();
38+
} catch {
39+
return null;
40+
}
41+
}
42+
43+
/**
44+
* Get the authenticated session with the User record from DB.
45+
* Memoized per request via React cache().
46+
*/
47+
export const getSessionWithUser = cache(async () => {
48+
const auth = await getAuthSession();
49+
const user = await prisma.user.findUnique({ where: { id: auth.userId } });
50+
if (user == null) {
51+
throw new UserNotFoundError(auth.userId);
52+
}
53+
return { ...auth, user };
54+
});
55+
56+
export class UserNotFoundError {
57+
constructor(public readonly userId: string) {}
3858
}

webapp/src/lib/safe-action.ts

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
1+
import { getAuthSession } from '@/lib/auth';
12
import { prisma } from '@/lib/prisma';
2-
import { runWithAmplifyServerContext } from '@/lib/amplifyServerUtils';
3-
import { getCurrentUser } from 'aws-amplify/auth/server';
43
import { createSafeActionClient, DEFAULT_SERVER_ERROR_MESSAGE } from 'next-safe-action';
5-
import { cookies } from 'next/headers';
64

75
export class MyCustomError extends Error {
86
constructor(message: string) {
@@ -28,18 +26,11 @@ const actionClient = createSafeActionClient({
2826
});
2927

3028
export const authActionClient = actionClient.use(async ({ next }) => {
31-
const currentUser = await runWithAmplifyServerContext({
32-
nextServerContext: { cookies },
33-
operation: (contextSpec) => getCurrentUser(contextSpec),
34-
});
35-
36-
if (!currentUser) {
37-
throw new Error('Session is not valid!');
38-
}
29+
const { userId } = await getAuthSession();
3930

4031
const user = await prisma.user.findUnique({
4132
where: {
42-
id: currentUser.userId,
33+
id: userId,
4334
},
4435
});
4536

0 commit comments

Comments
 (0)