Skip to content

Commit 4e6ee03

Browse files
authored
Merge pull request #83 from hammercode-dev/auth
Update auth implementation
2 parents 76c90fd + 7d99de7 commit 4e6ee03

21 files changed

Lines changed: 194 additions & 152 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",

src/app/[locale]/(public)/(user)/layout.tsx

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,26 @@
1+
"use client";
2+
13
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/Avatar";
24

35
import { Calendar1, User } from "lucide-react";
46
import { Separator } from "@/components/ui/Separator";
57
import { Link } from "@/lib/navigation";
8+
import { useAuthUser } from "@/components/hooks/UseAuthUser";
69

7-
const user = {
8-
name: "Putra Satria",
9-
image: "",
10-
fallback: "PS",
11-
};
12-
13-
export default async function UserLayout({ children }: { children: React.ReactNode }) {
10+
export default function UserLayout({ children }: { children: React.ReactNode }) {
11+
const { user } = useAuthUser();
1412
return (
1513
<section className="container mx-auto px-5 pt-24 pb-28">
1614
<div className="grid grid-cols-5 gap-8">
1715
<aside className="fixed right-0 bottom-0 left-0 col-span-1 mt-8 flex w-full flex-col justify-between gap-4 self-start rounded-lg bg-white lg:sticky lg:top-24 lg:flex-col lg:justify-start lg:bg-transparent dark:bg-slate-950">
1816
<div className="flex items-center gap-4">
1917
<Avatar className="h-12 w-12">
20-
<AvatarImage src={user.image} />
21-
<AvatarFallback>{user.fallback}</AvatarFallback>
18+
<AvatarImage src="" />
19+
<AvatarFallback>US</AvatarFallback>
2220
</Avatar>
2321
<div>
24-
<p className="text-muted-foreground text-sm">Hi</p>
25-
<p className="font-semibold">{user.name}</p>
22+
<p className="text-muted-foreground text-sm">Hello</p>
23+
<p className="font-semibold">{user?.username}</p>
2624
</div>
2725
</div>
2826

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/events/page.tsx

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
1+
"use client";
2+
3+
import { useEvents } from "@/features/events/hooks/useEvent";
14
import Link from "next/link";
25

36
const EventListPage = () => {
7+
const { events, isLoading } = useEvents();
48
return (
59
<div>
610
<h1>Event List</h1>
7-
11+
{isLoading && <p>Fetching events...</p>}
812
<ul>
9-
<li>
10-
Event 123 <Link href="/admin/events/123/edit">Edit</Link>
11-
</li>
12-
<li>
13-
Event 456 <Link href="/admin/events/456/edit">Edit</Link>
14-
</li>
13+
{events.map((ev) => (
14+
<li key={ev.id}>
15+
{ev.title} <Link href={`/admin/events/${ev.id}/edit`}>Edit</Link>
16+
</li>
17+
))}
1518
</ul>
1619
</div>
1720
);

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

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
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+
import { jwtDecode } from "jwt-decode";
6+
import { redirect } from "next/navigation";
7+
import { AuthJwtPayload } from "@/types";
88
import { Separator } from "@/components/ui/Separator";
99
import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/Sidebar";
1010

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

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

2020
return (
2121
<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 token = await authService.getToken({ email, password });
11+
const decoded = jwtDecode<AuthJwtPayload>(token);
12+
13+
const res = NextResponse.json({
14+
token,
15+
payload: decoded,
16+
});
17+
// TODO: fix(security)
18+
// set cookie with these attributes: same-site
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: 8 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">
@@ -41,6 +41,11 @@ const DesktopUserMenu = () => {
4141
</div>
4242
</Link>
4343
</DropdownMenuItem>
44+
{user?.role === "admin" && (
45+
<DropdownMenuItem asChild>
46+
<Link href="/admin/events">Dashboard</Link>
47+
</DropdownMenuItem>
48+
)}
4449
{USER_LINKS.map(({ id, href }) => (
4550
<DropdownMenuItem key={id} asChild>
4651
<Link href={href}>{t(`navbar.user.${id}`)}</Link>
@@ -60,7 +65,7 @@ const DesktopUserMenu = () => {
6065
const MobileUserMenu = () => {
6166
const t = useTranslations("Layout");
6267
const { user, isAuthenticated } = useAuthUser();
63-
const { logout } = useAuth();
68+
const { logout } = useAuthService();
6469

6570
return (
6671
<div className="border-t pt-4">

src/components/layout/WrapperLayout/constant.ts

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

0 commit comments

Comments
 (0)