Skip to content

Commit d8a27cc

Browse files
committed
feat(auth): Add AuthProvider and TokenProvider for user authentication and token management
1 parent 5a47c1b commit d8a27cc

File tree

6 files changed

+162
-10
lines changed

6 files changed

+162
-10
lines changed

app/features/login/index.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,16 @@ import { Lock, Mail, Eye, EyeOff, BookOpen } from "lucide-react";
1919
import { useToast } from "@/hooks/use-toast";
2020
import { useToggle } from "@/hooks/useToggle";
2121

22+
import { useAuth } from "@/providers/AuthProvider";
2223
import { useSignin } from "@/service/auth";
2324

2425
import { loginSchema, defaultValues } from "./data/schema";
2526

2627
type LoginFormValues = z.infer<typeof loginSchema>;
2728

2829
export default function Login() {
30+
const { handleLogin } = useAuth();
31+
2932
const { toast } = useToast();
3033
const { open: isPasswordVisibility, toggle: togglePasswordVisibility } =
3134
useToggle(false);
@@ -46,9 +49,10 @@ export default function Login() {
4649
try {
4750
const response = await trigger({ data });
4851
const { token, expired } = response;
49-
document.cookie = `hexToken=${token};expires=${new Date(
50-
String(expired)
51-
).toUTCString()};`;
52+
handleLogin({
53+
token: String(token),
54+
expired: Number(expired),
55+
});
5256

5357
toast({
5458
title: "登入成功",

app/hooks/useApiFactory.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,12 @@ export const createApiHooks = <T extends EndpointConfig>(endpoints: T): HooksTyp
5050
return (params?: GetParams, swrConfig?: SWRConfiguration) => {
5151
return useSWR<APIResponse<TData>>(
5252
params ? [path, params] : path,
53-
async () => fetcher<TData>(path, { method: 'GET', params }),
53+
async (url) => {
54+
return fetcher(url, {
55+
method,
56+
params: params,
57+
});
58+
},
5459
swrConfig
5560
);
5661
};
@@ -77,13 +82,12 @@ export const createApiHooks = <T extends EndpointConfig>(endpoints: T): HooksTyp
7782
MutationParams<TData>
7883
>(
7984
path,
80-
async (url: string, { arg }: { arg?: MutationParams<TData> }) => {
81-
return fetcher<TData>(url, {
85+
async (url: string, { arg }: { arg?: MutationParams<TData> }) =>
86+
fetcher(url, {
8287
method,
8388
params: arg?.params,
8489
data: arg?.data,
85-
});
86-
},
90+
}),
8791
mutationConfig
8892
);
8993

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import {
2+
createContext,
3+
useContext,
4+
useState,
5+
useEffect,
6+
useCallback,
7+
} from "react";
8+
9+
import { useCheckUser } from "~/service/auth";
10+
11+
type LoginResponseType = {
12+
token: string;
13+
expired: number;
14+
};
15+
16+
type AuthContextType = {
17+
isLogin: boolean;
18+
isLoading: boolean;
19+
handleLogin: (response: LoginResponseType) => void;
20+
};
21+
22+
const AuthContext = createContext<AuthContextType>({
23+
isLogin: false,
24+
isLoading: false,
25+
handleLogin: () => {},
26+
});
27+
28+
export default function AuthProvider({
29+
children,
30+
}: {
31+
children: React.ReactNode;
32+
}) {
33+
const [isLogin, setIsLogin] = useState<boolean>(false);
34+
35+
const { trigger, isMutating } = useCheckUser();
36+
37+
useEffect(() => {
38+
const abortController = new AbortController();
39+
40+
const checkUserStatus = async () => {
41+
const token = document.cookie
42+
.split("; ")
43+
.find((row) => row.startsWith("hexToken="))
44+
?.split("=")[1];
45+
46+
if (!token) {
47+
setIsLogin(false);
48+
return;
49+
}
50+
51+
try {
52+
const response = await trigger();
53+
if (response.success) {
54+
setIsLogin(true);
55+
} else {
56+
setIsLogin(false);
57+
}
58+
} catch (error) {
59+
if (!abortController.signal.aborted) {
60+
console.log("檢查使用者狀態時發生錯誤:", error);
61+
setIsLogin(false);
62+
}
63+
}
64+
};
65+
66+
checkUserStatus();
67+
68+
return () => {
69+
abortController.abort();
70+
};
71+
}, [trigger]);
72+
73+
const handleLogin = useCallback(
74+
(response: LoginResponseType) => {
75+
setIsLogin(true);
76+
77+
const { token, expired } = response;
78+
document.cookie = `hexToken=${token};expires=${new Date(
79+
expired
80+
).toUTCString()};`;
81+
},
82+
[setIsLogin]
83+
);
84+
85+
return (
86+
<AuthContext.Provider
87+
value={{ isLogin, isLoading: isMutating, handleLogin }}
88+
>
89+
{children}
90+
</AuthContext.Provider>
91+
);
92+
}
93+
94+
export function useAuth() {
95+
const context = useContext(AuthContext);
96+
97+
if (context === undefined) {
98+
throw new Error("useAuth must be used within a AuthProvider");
99+
}
100+
101+
return context;
102+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { SWRConfig, type Middleware, type SWRHook } from "swr";
2+
3+
const tokenMiddleware: Middleware = (useSWRNext: SWRHook) => {
4+
return (key, fetcher, config) => {
5+
const token =
6+
typeof document !== "undefined"
7+
? document.cookie
8+
.split("; ")
9+
.find((row) => row.startsWith("hexToken="))
10+
?.split("=")[1]
11+
: undefined;
12+
13+
const withToken = key
14+
? [
15+
key,
16+
{
17+
headers: {
18+
Authorization: `${token}`,
19+
},
20+
},
21+
]
22+
: null;
23+
24+
const swr = useSWRNext(withToken, fetcher, config);
25+
return swr;
26+
};
27+
};
28+
29+
export default function TokenProvider(props: { children: React.ReactNode }) {
30+
return (
31+
<SWRConfig value={{ use: [tokenMiddleware] }}>{props.children}</SWRConfig>
32+
);
33+
}

app/root.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ import { Toaster } from "@/components/ui/toaster";
1111

1212
import stylesheet from "./app.css?url";
1313

14+
import TokenProvider from "./providers/TokenProvider";
15+
import AuthProvider from "./providers/AuthProvider";
16+
1417
export const links: Route.LinksFunction = () => [
1518
{ rel: "preconnect", href: "https://fonts.googleapis.com" },
1619
{
@@ -36,7 +39,12 @@ export function Layout({ children }: { children: React.ReactNode }) {
3639
</head>
3740

3841
<body>
39-
{children}
42+
<TokenProvider>
43+
<AuthProvider>
44+
<>{children}</>
45+
</AuthProvider>
46+
</TokenProvider>
47+
4048
<Toaster />
4149
<ScrollRestoration />
4250
<Scripts />

app/utils/fetcher.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
const API_PREFIX = '/v2';
22

33
const createUrl = (path: string, params?: Record<string, string | number>) => {
4-
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
4+
const normalizedPath = path[0].startsWith('/') ? path[0] : `/${path[0]}`;
55
const fullPath = `${API_PREFIX}${normalizedPath}`;
66

77
if (!params) {
@@ -26,6 +26,7 @@ export const fetcher = async <T>(
2626
headers: {
2727
'Content-Type': 'application/json',
2828
...headers,
29+
...(Array.isArray(path) ? path[1]?.headers : {}),
2930
},
3031
body: data ? JSON.stringify(data) : undefined,
3132
...config,

0 commit comments

Comments
 (0)