Skip to content

Commit 78b3dc5

Browse files
committed
refactor(auth): refactor auth implementation
1 parent 76c90fd commit 78b3dc5

18 files changed

Lines changed: 157 additions & 132 deletions

File tree

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"axios": "^1.7.7",
3535
"class-variance-authority": "^0.7.1",
3636
"clsx": "^2.1.1",
37+
"cookie": "^1.0.2",
3738
"embla-carousel-autoplay": "^8.2.0",
3839
"embla-carousel-react": "^8.2.0",
3940
"jwt-decode": "^4.0.0",
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
"use client";
2+
3+
import Navbar from "@/components/layout/Navbar";
4+
import Footer from "@/components/layout/Footer";
5+
6+
import { useParams, usePathname } from "next/navigation";
7+
const authPaths = ["sign-in", "sign-up", "forgot-password", "reset-password"];
8+
9+
const WrapperLayout = ({ children }: { children: React.ReactNode }) => {
10+
const params = useParams();
11+
const pathname = usePathname();
12+
const isAuthPage = authPaths.some((path) => pathname.includes(path));
13+
const isCertificateDetailPage = !!params?.slug && pathname.includes("certificates");
14+
15+
if (isAuthPage || isCertificateDetailPage) {
16+
return children;
17+
}
18+
19+
return (
20+
<>
21+
<Navbar />
22+
{children}
23+
<Footer />
24+
</>
25+
);
26+
};
27+
export default WrapperLayout;

src/app/[locale]/admin/layout.tsx

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,22 @@
1-
"use client";
2-
3-
import { useEffect } from "react";
4-
import { redirect } from "next/navigation";
1+
import { cookies } from "next/headers";
52
import RouteBreadcrumb from "@/components/common/RouteBreadcrumb";
6-
import { useAuthUser } from "@/components/hooks/UseAuthUser";
73
import AdminSidebar from "@/components/layout/AdminSidebar";
4+
5+
6+
import { jwtDecode } from "jwt-decode";
7+
import { redirect } from "next/navigation";
8+
import { AuthJwtPayload } from "@/types";
89
import { Separator } from "@/components/ui/Separator";
910
import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/Sidebar";
1011

11-
export default function AdminLayout({ children }: { children: React.ReactNode }) {
12-
const { user, isLoading } = useAuthUser();
12+
export default async function AdminLayout({ children }: { children: React.ReactNode }) {
13+
const cookieStore = await cookies();
14+
const token = cookieStore.get("token");
15+
16+
if (!token) return redirect("/");
1317

14-
useEffect(() => {
15-
if (!isLoading && (!user || user.role !== "admin")) {
16-
redirect("/");
17-
}
18-
}, [isLoading, user]);
18+
const payload = jwtDecode<AuthJwtPayload>(token.value);
19+
if (payload.role !== "user") return redirect("/");
1920

2021
return (
2122
<SidebarProvider>

src/app/[locale]/layout.tsx

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import { NextIntlClientProvider } from "next-intl";
22
import { getMessages, getTranslations, setRequestLocale } from "next-intl/server";
33
import { Sora } from "next/font/google";
4-
import WrapperLayout from "@/components/layout/WrapperLayout";
54
import { locales } from "@/lib/locales";
65
import { notFound } from "next/navigation";
76
import { Toaster } from "@/components/ui/Toaster";
7+
import { AuthProvider } from "@/components/provider/AuthProvider";
8+
import { cookies } from "next/headers";
9+
import { AuthJwtPayload } from "@/types";
10+
import { jwtDecode } from "jwt-decode";
11+
import { ThemeProvider } from "@/components/provider/ThemeProvider";
812
const sora = Sora({ subsets: ["latin"] });
913

1014
type Props = {
@@ -33,22 +37,31 @@ export async function generateMetadata(props: Props) {
3337

3438
export default async function LocaleRootLayout(props: Readonly<Props>) {
3539
const params = await props.params;
36-
3740
const { locale } = params;
38-
3941
const { children } = props;
4042

4143
if (!locales.includes(locale)) notFound();
4244

4345
setRequestLocale(locale);
4446

47+
// Get authenticated user info
48+
// This data is retrieved on server context
49+
// Then we passed it into auth provider
50+
const cookieStore = await cookies();
51+
const token = cookieStore.get("token");
52+
const payload: AuthJwtPayload | undefined = token?.value ? jwtDecode<AuthJwtPayload>(token.value) : undefined;
53+
4554
const messages = await getMessages();
4655

4756
return (
4857
<html lang={locale} suppressHydrationWarning>
4958
<body className={`${sora.className}`}>
5059
<NextIntlClientProvider messages={messages}>
51-
<WrapperLayout>{children}</WrapperLayout>
60+
<AuthProvider payload={payload}>
61+
<ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
62+
{children}
63+
</ThemeProvider>
64+
</AuthProvider>
5265
<Toaster />
5366
</NextIntlClientProvider>
5467
</body>

src/app/api/login/route.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { authService } from "@/services/auth";
2+
import { AuthJwtPayload } from "@/types";
3+
import { jwtDecode } from "jwt-decode";
4+
import { NextResponse } from "next/server";
5+
6+
export async function POST(request: Request) {
7+
try {
8+
const body = await request.json();
9+
const { email, password } = body;
10+
const result = await authService.getToken({ email, password });
11+
const token = result.data;
12+
const decoded = jwtDecode<AuthJwtPayload>(token);
13+
14+
const res = NextResponse.json(token);
15+
// TODO: fix(security) set cookie with these attributes
16+
// httpOnly=true, secure=true if prod, same-site
17+
// max-age to follow the token. extract from token
18+
// TODO: create logout endpoint and remove cookie there
19+
20+
// Now server components will have access to token
21+
// Question: should we encrypt token?
22+
const now = Math.floor(Date.now() / 1000);
23+
const maxAge = decoded.exp ? decoded.exp - now : undefined;
24+
res.cookies.set("token", token, {
25+
httpOnly: true,
26+
secure: process.env.NODE_ENV === "production",
27+
maxAge,
28+
});
29+
return res;
30+
} catch (error) {
31+
// if (error.type === 'CredentialsSignin') {
32+
// return NextResponse.json({ error: 'Invalid credentials.' }, { status: 401 })
33+
// } else {
34+
// NextResponse.json({ error: 'Something went wrong.' }, { status: 500 })
35+
// }
36+
return NextResponse.json({ error }, { status: 500 });
37+
}
38+
}

src/app/api/logout/route.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { NextResponse } from "next/server";
2+
3+
export async function POST() {
4+
const res = NextResponse.json({ success: true });
5+
res.cookies.set("token", "", {
6+
httpOnly: true,
7+
secure: process.env.NODE_ENV === "production",
8+
maxAge: 0,
9+
path: "/",
10+
});
11+
return res;
12+
}

src/components/layout/Navbar/UserMenu.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@ import { ChevronLeft, LogOut, User } from "lucide-react";
55
import { USER_LINKS } from "./constant";
66
import { useTranslations } from "next-intl";
77
import { useAuthUser } from "@/components/hooks/UseAuthUser";
8-
import { useAuth } from "@/features/auth/hooks/useAuth";
8+
import { useAuthService } from "@/features/auth/hooks/useAuth";
99

1010
const DesktopUserMenu = () => {
1111
const t = useTranslations("Layout");
1212
const { user, isAuthenticated } = useAuthUser();
13-
const { logout } = useAuth();
13+
const { logout } = useAuthService();
1414

1515
return !isAuthenticated ? (
1616
<Button asChild size="sm" className="w-full">
@@ -60,7 +60,7 @@ const DesktopUserMenu = () => {
6060
const MobileUserMenu = () => {
6161
const t = useTranslations("Layout");
6262
const { user, isAuthenticated } = useAuthUser();
63-
const { logout } = useAuth();
63+
const { logout } = useAuthService();
6464

6565
return (
6666
<div className="border-t pt-4">

src/components/layout/WrapperLayout/constant.ts

Lines changed: 0 additions & 1 deletion
This file was deleted.

src/components/layout/WrapperLayout/index.tsx

Lines changed: 0 additions & 33 deletions
This file was deleted.
Lines changed: 14 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,22 @@
11
"use client";
22

3-
import { createContext, ReactNode, useEffect, useState } from "react";
4-
import { User, UserContextType } from "@/types";
5-
import { decodeToken } from "@/lib/jwt";
6-
// import { profileService } from "@/services/profile";
3+
import { createContext, ReactNode } from "react";
4+
import { AuthJwtPayload, UserContextType } from "@/types";
75

86
export const UserContext = createContext<UserContextType | undefined>(undefined);
97

10-
export const AuthProvider = ({ children }: { children: ReactNode }) => {
11-
const [user, setUser] = useState<User | null>(null);
12-
const [isLoading, setIsLoading] = useState(true);
8+
interface AuthProviderProps {
9+
/**
10+
* JWT Payload taken from cookie
11+
* @default undefined
12+
*/
13+
payload?: AuthJwtPayload;
14+
children: ReactNode;
15+
}
1316

14-
const getUserProfile = async () => {
15-
setIsLoading(true);
16-
const token = await localStorage.getItem("accessToken");
17-
if (!token) return;
17+
export const AuthProvider = ({ payload, children }: AuthProviderProps) => {
18+
const user = payload || null;
19+
const isAuthenticated = !!user;
1820

19-
const { isTokenExpired, username, role, email } = decodeToken(token);
20-
console.log(decodeToken(token));
21-
if (isTokenExpired) {
22-
localStorage.removeItem("accessToken");
23-
return;
24-
}
25-
26-
try {
27-
// const { data: user } = await profileService.getUserId();
28-
29-
setUser({
30-
username: username,
31-
email: email,
32-
role: role,
33-
// phone_number: phone_number || '',
34-
});
35-
} catch (err) {
36-
console.error("Failed to fetch user profile:", err);
37-
localStorage.removeItem("accessToken");
38-
} finally {
39-
setIsLoading(false);
40-
}
41-
};
42-
43-
useEffect(() => {
44-
getUserProfile();
45-
}, []);
46-
47-
return (
48-
<UserContext.Provider
49-
value={{
50-
user,
51-
setUser,
52-
isLoading,
53-
isAuthenticated: !!user,
54-
}}
55-
>
56-
{children}
57-
</UserContext.Provider>
58-
);
21+
return <UserContext.Provider value={{ user, isAuthenticated, isLoading: false }}>{children}</UserContext.Provider>;
5922
};

0 commit comments

Comments
 (0)