Skip to content

Commit ff68eaa

Browse files
committed
refactor: clean up
1 parent c1fe7ee commit ff68eaa

File tree

12 files changed

+125
-112
lines changed

12 files changed

+125
-112
lines changed

app/(admin)/layout.tsx

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,25 @@
11
import { AppSidebar } from "@/components/app-sidebar";
22
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
3+
import { UserProvider } from "@/providers/use-user";
34

45
export default function AdminLayout({
56
children,
67
}: Readonly<{
78
children: React.ReactNode;
89
}>) {
910
return (
10-
<SidebarProvider
11-
style={{
12-
"--sidebar-width": "calc(var(--spacing) * 72)",
13-
"--header-height": "calc(var(--spacing) * 12)",
14-
} as React.CSSProperties}
15-
>
16-
<AppSidebar variant="inset" />
17-
<SidebarInset>
18-
{children}
19-
</SidebarInset>
20-
</SidebarProvider>
11+
<UserProvider>
12+
<SidebarProvider
13+
style={{
14+
"--sidebar-width": "calc(var(--spacing) * 72)",
15+
"--header-height": "calc(var(--spacing) * 12)",
16+
} as React.CSSProperties}
17+
>
18+
<AppSidebar variant="inset" />
19+
<SidebarInset>
20+
{children}
21+
</SidebarInset>
22+
</SidebarProvider>
23+
</UserProvider>
2124
);
2225
}

app/api/auth/login/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import {
44
generateCodeVerifier,
55
generateState,
66
OAUTH_CONFIG,
7-
redirectIfAuthenticated,
87
setOAuthState,
98
} from "@/lib/auth";
9+
import { redirectIfAuthenticated } from "@/lib/auth.rsc";
1010

1111
/**
1212
* OAuth 2.0 Authorization Code Flow with PKCE - Login Initiation

components/auth-guard/forbidden.tsx renamed to app/forbidden/page.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1+
import { Logo } from "@/components/logo";
12
import { Button } from "@/components/ui/button";
23
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
3-
import type { BasicUserInfo } from "@/lib/user";
4+
import { redirectIfAuthenticated } from "@/lib/auth.rsc";
45
import { AlertTriangle } from "lucide-react";
56
import Link from "next/link";
6-
import { Logo } from "../logo";
77

8-
export default function Forbidden({ user }: { user: BasicUserInfo }) {
8+
export default async function ForbiddenPage({ searchParams }: { searchParams: Promise<{ name?: string; email?: string }> }) {
9+
await redirectIfAuthenticated();
10+
11+
const { name, email } = await searchParams;
12+
913
return (
1014
<div
1115
className={`
@@ -38,14 +42,14 @@ export default function Forbidden({ user }: { user: BasicUserInfo }) {
3842
</CardHeader>
3943
<CardContent className="flex flex-col items-center gap-4">
4044
<Button asChild variant="outline">
41-
<Link href="/">回到主程式</Link>
45+
<Link href="/login">重新登入</Link>
4246
</Button>
4347
</CardContent>
4448
<CardFooter
4549
className={`justify-center text-center text-xs text-muted-foreground`}
4650
>
4751
<section className="flex flex-col items-center gap-1">
48-
<p>您目前登入的帳號是:{user.name} ({user.email})</p>
52+
<p>您目前登入的帳號是:{name} ({email})</p>
4953
<p>如果這不是您想登入的帳號,請切換 Google 帳號後重新登入</p>
5054
</section>
5155
</CardFooter>

app/layout.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,7 @@ export default function RootLayout({
4444
`}
4545
>
4646
<ApolloWrapper>
47-
<UserProvider>
48-
{children}
49-
</UserProvider>
47+
{children}
5048
</ApolloWrapper>
5149
<Toaster />
5250
</body>

app/login/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { LoginForm } from "@/components/login-form";
22
import { Logo } from "@/components/logo";
3-
import { redirectIfAuthenticated } from "@/lib/auth";
3+
import { redirectIfAuthenticated } from "@/lib/auth.rsc";
44
import Link from "next/link";
55

66
interface LoginPageProps {

components/auth-guard/index.tsx

Lines changed: 0 additions & 28 deletions
This file was deleted.

components/login-form.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ function getErrorMessage(error: string, description?: string): string {
8686
return "認證過程中發生錯誤,請重新登入。";
8787
case "logout_failed":
8888
return "登出時發生錯誤,但您的本地工作階段已清除。";
89+
case "forbidden":
90+
return "您沒有權限存取此應用程式。";
8991
default:
9092
return "登入時發生未知錯誤,請重試。";
9193
}

lib/apollo.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,9 @@
11
import { HttpLink } from "@apollo/client";
22
import { ApolloClient, InMemoryCache } from "@apollo/client-integration-nextjs";
3+
import buildUri from "./build-uri";
34

45
/**
56
* Creates an Apollo Client instance that uses the GraphQL proxy API.
6-
*
7-
* This implementation follows the Backend for Frontend (BFF) pattern:
8-
* - All GraphQL requests go through /api/graphql proxy
9-
* - The proxy automatically handles authentication by adding Authorization headers
10-
* - Works consistently for both SSR and CSR without exposing tokens to the client
11-
* - Removes the need for client-side token management
127
*/
138
export function makeClient() {
149
const httpLink = new HttpLink({
@@ -22,3 +17,22 @@ export function makeClient() {
2217
link: httpLink,
2318
});
2419
}
20+
21+
/**
22+
* Create an Apollo Client instance that uses the upstream GraphQL API.
23+
*
24+
* You should add the token to the headers of the request.
25+
*/
26+
export function makeUpstreamClient({ token }: { token?: string | null }) {
27+
const httpLink = new HttpLink({
28+
uri: buildUri("/query"),
29+
headers: {
30+
Authorization: token ? `Bearer ${token}` : "",
31+
},
32+
});
33+
34+
return new ApolloClient({
35+
cache: new InMemoryCache(),
36+
link: httpLink,
37+
});
38+
}

lib/auth.rsc.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { redirect } from "next/navigation";
2+
import { getAuthStatus } from "./auth";
3+
4+
export async function redirectIfAuthenticated(): Promise<void> {
5+
const isAuthenticated = await getAuthStatus();
6+
if (isAuthenticated.role === 'admin') {
7+
redirect("/");
8+
}
9+
}

lib/auth.ts

Lines changed: 45 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { cookies } from "next/headers";
2-
import { redirect } from "next/navigation";
32
import { getIronSession } from "iron-session";
43
import buildUri from "./build-uri";
4+
import { makeUpstreamClient } from "./apollo";
5+
import { BASIC_USER_INFO_QUERY, isAdmin, type BasicUserInfo } from "./user";
6+
import { CombinedGraphQLErrors } from "@apollo/client";
57

68
// Constants for OAuth 2.0 PKCE flow
79
export const OAUTH_CONFIG = {
@@ -20,7 +22,6 @@ export interface SessionData {
2022
access_token?: string;
2123
token_type?: string;
2224
expires_in?: number;
23-
isLoggedIn: boolean;
2425
}
2526

2627
if (!process.env.AUTH_SECRET) {
@@ -83,15 +84,14 @@ export async function setAuthToken(
8384
session.access_token = token;
8485
session.token_type = tokenType;
8586
session.expires_in = expiresIn;
86-
session.isLoggedIn = true;
8787

8888
await session.save();
8989
}
9090

9191
export async function getAuthToken(): Promise<string | null> {
9292
const session = await getSession();
9393

94-
if (!session.isLoggedIn || !session.access_token) {
94+
if (!session.access_token) {
9595
return null;
9696
}
9797

@@ -221,19 +221,49 @@ export async function revokeToken(token: string): Promise<void> {
221221
}
222222

223223
// Auth validation
224-
export async function validateAuth(): Promise<boolean> {
225-
const token = await getAuthToken();
226-
return token !== null;
224+
export interface AuthStatus {
225+
loggedIn: boolean;
226+
227+
role?: "admin" | "user";
228+
user?: BasicUserInfo;
227229
}
228230

229-
// Redirect helpers
230-
export async function requireAuth(): Promise<never> {
231-
redirect("/login");
232-
}
231+
export async function getAuthStatus(): Promise<AuthStatus> {
232+
const token = await getAuthToken();
233+
if (!token) {
234+
return {
235+
loggedIn: false,
236+
};
237+
}
238+
239+
// get user info
240+
const client = makeUpstreamClient({ token });
241+
242+
try {
243+
const { data } = await client.query({
244+
query: BASIC_USER_INFO_QUERY,
245+
});
246+
if (!data) {
247+
return {
248+
loggedIn: true,
249+
};
250+
}
251+
252+
return {
253+
role: isAdmin(data?.me) ? "admin" : "user",
254+
user: data.me,
255+
loggedIn: true,
256+
};
257+
} catch (error) {
258+
if (CombinedGraphQLErrors.is(error) && error.message === 'require authentication') {
259+
return {
260+
loggedIn: false,
261+
};
262+
}
233263

234-
export async function redirectIfAuthenticated(): Promise<void> {
235-
const isAuthenticated = await validateAuth();
236-
if (isAuthenticated) {
237-
redirect("/");
264+
console.log("Error validating auth:", error);
265+
return {
266+
loggedIn: false,
267+
};
238268
}
239269
}

0 commit comments

Comments
 (0)