Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"axios": "^1.7.7",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cookie": "^1.0.2",
"embla-carousel-autoplay": "^8.2.0",
"embla-carousel-react": "^8.2.0",
"jwt-decode": "^4.0.0",
Expand Down
20 changes: 9 additions & 11 deletions src/app/[locale]/(public)/(user)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,26 @@
"use client";

import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/Avatar";

import { Calendar1, User } from "lucide-react";
import { Separator } from "@/components/ui/Separator";
import { Link } from "@/lib/navigation";
import { useAuthUser } from "@/components/hooks/UseAuthUser";

const user = {
name: "Putra Satria",
image: "",
fallback: "PS",
};

export default async function UserLayout({ children }: { children: React.ReactNode }) {
export default function UserLayout({ children }: { children: React.ReactNode }) {
const { user } = useAuthUser();
return (
<section className="container mx-auto px-5 pt-24 pb-28">
<div className="grid grid-cols-5 gap-8">
<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">
<div className="flex items-center gap-4">
<Avatar className="h-12 w-12">
<AvatarImage src={user.image} />
<AvatarFallback>{user.fallback}</AvatarFallback>
<AvatarImage src="" />
<AvatarFallback>US</AvatarFallback>
</Avatar>
<div>
<p className="text-muted-foreground text-sm">Hi</p>
<p className="font-semibold">{user.name}</p>
<p className="text-muted-foreground text-sm">Hello</p>
<p className="font-semibold">{user?.username}</p>
</div>
</div>

Expand Down
27 changes: 27 additions & 0 deletions src/app/[locale]/(public)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"use client";

import Navbar from "@/components/layout/Navbar";
import Footer from "@/components/layout/Footer";

import { useParams, usePathname } from "next/navigation";
const authPaths = ["sign-in", "sign-up", "forgot-password", "reset-password"];

const WrapperLayout = ({ children }: { children: React.ReactNode }) => {
const params = useParams();
const pathname = usePathname();
const isAuthPage = authPaths.some((path) => pathname.includes(path));
const isCertificateDetailPage = !!params?.slug && pathname.includes("certificates");

if (isAuthPage || isCertificateDetailPage) {
return children;
}

return (
<>
<Navbar />
{children}
<Footer />
</>
);
};
export default WrapperLayout;
17 changes: 10 additions & 7 deletions src/app/[locale]/admin/events/page.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
"use client";

import { useEvents } from "@/features/events/hooks/useEvent";
import Link from "next/link";

const EventListPage = () => {
const { events, isLoading } = useEvents();
return (
<div>
<h1>Event List</h1>

{isLoading && <p>Fetching events...</p>}
<ul>
<li>
Event 123 <Link href="/admin/events/123/edit">Edit</Link>
</li>
<li>
Event 456 <Link href="/admin/events/456/edit">Edit</Link>
</li>
{events.map((ev) => (
<li key={ev.id}>
{ev.title} <Link href={`/admin/events/${ev.id}/edit`}>Edit</Link>
</li>
))}
</ul>
</div>
);
Expand Down
24 changes: 12 additions & 12 deletions src/app/[locale]/admin/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
"use client";

import { useEffect } from "react";
import { redirect } from "next/navigation";
import { cookies } from "next/headers";
import RouteBreadcrumb from "@/components/common/RouteBreadcrumb";
import { useAuthUser } from "@/components/hooks/UseAuthUser";
import AdminSidebar from "@/components/layout/AdminSidebar";

import { jwtDecode } from "jwt-decode";
import { redirect } from "next/navigation";
import { AuthJwtPayload } from "@/types";
import { Separator } from "@/components/ui/Separator";
import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/Sidebar";

export default function AdminLayout({ children }: { children: React.ReactNode }) {
const { user, isLoading } = useAuthUser();
export default async function AdminLayout({ children }: { children: React.ReactNode }) {
const cookieStore = await cookies();
const token = cookieStore.get("token");

if (!token) return redirect("/");

useEffect(() => {
if (!isLoading && (!user || user.role !== "admin")) {
redirect("/");
}
}, [isLoading, user]);
const payload = jwtDecode<AuthJwtPayload>(token.value);
if (payload.role !== "admin") return redirect("/");

return (
<SidebarProvider>
Expand Down
21 changes: 17 additions & 4 deletions src/app/[locale]/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { NextIntlClientProvider } from "next-intl";
import { getMessages, getTranslations, setRequestLocale } from "next-intl/server";
import { Sora } from "next/font/google";
import WrapperLayout from "@/components/layout/WrapperLayout";
import { locales } from "@/lib/locales";
import { notFound } from "next/navigation";
import { Toaster } from "@/components/ui/Toaster";
import { AuthProvider } from "@/components/provider/AuthProvider";
import { cookies } from "next/headers";
import { AuthJwtPayload } from "@/types";
import { jwtDecode } from "jwt-decode";
import { ThemeProvider } from "@/components/provider/ThemeProvider";
const sora = Sora({ subsets: ["latin"] });

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

export default async function LocaleRootLayout(props: Readonly<Props>) {
const params = await props.params;

const { locale } = params;

const { children } = props;

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

setRequestLocale(locale);

// Get authenticated user info
// This data is retrieved on server context
// Then we passed it into auth provider
const cookieStore = await cookies();
const token = cookieStore.get("token");
const payload: AuthJwtPayload | undefined = token?.value ? jwtDecode<AuthJwtPayload>(token.value) : undefined;

const messages = await getMessages();

return (
<html lang={locale} suppressHydrationWarning>
<body className={`${sora.className}`}>
<NextIntlClientProvider messages={messages}>
<WrapperLayout>{children}</WrapperLayout>
<AuthProvider payload={payload}>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
{children}
</ThemeProvider>
</AuthProvider>
<Toaster />
</NextIntlClientProvider>
</body>
Expand Down
38 changes: 38 additions & 0 deletions src/app/api/login/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { authService } from "@/services/auth";
import { AuthJwtPayload } from "@/types";
import { jwtDecode } from "jwt-decode";
import { NextResponse } from "next/server";

export async function POST(request: Request) {
try {
const body = await request.json();
const { email, password } = body;
const token = await authService.getToken({ email, password });
const decoded = jwtDecode<AuthJwtPayload>(token);

const res = NextResponse.json({
token,
payload: decoded,
});
// TODO: fix(security)
// set cookie with these attributes: same-site

// Now server components will have access to token
// Question: should we encrypt token?
const now = Math.floor(Date.now() / 1000);
const maxAge = decoded.exp ? decoded.exp - now : undefined;
res.cookies.set("token", token, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
maxAge,
});
return res;
} catch (error) {
// if (error.type === 'CredentialsSignin') {
// return NextResponse.json({ error: 'Invalid credentials.' }, { status: 401 })
// } else {
// NextResponse.json({ error: 'Something went wrong.' }, { status: 500 })
// }
return NextResponse.json({ error }, { status: 500 });
}
}
12 changes: 12 additions & 0 deletions src/app/api/logout/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { NextResponse } from "next/server";

export async function POST() {
const res = NextResponse.json({ success: true });
res.cookies.set("token", "", {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
maxAge: 0,
path: "/",
});
return res;
}
11 changes: 8 additions & 3 deletions src/components/layout/Navbar/UserMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import { ChevronLeft, LogOut, User } from "lucide-react";
import { USER_LINKS } from "./constant";
import { useTranslations } from "next-intl";
import { useAuthUser } from "@/components/hooks/UseAuthUser";
import { useAuth } from "@/features/auth/hooks/useAuth";
import { useAuthService } from "@/features/auth/hooks/useAuth";

const DesktopUserMenu = () => {
const t = useTranslations("Layout");
const { user, isAuthenticated } = useAuthUser();
const { logout } = useAuth();
const { logout } = useAuthService();

return !isAuthenticated ? (
<Button asChild size="sm" className="w-full">
Expand Down Expand Up @@ -41,6 +41,11 @@ const DesktopUserMenu = () => {
</div>
</Link>
</DropdownMenuItem>
{user?.role === "admin" && (
<DropdownMenuItem asChild>
<Link href="/admin/events">Dashboard</Link>
</DropdownMenuItem>
)}
{USER_LINKS.map(({ id, href }) => (
<DropdownMenuItem key={id} asChild>
<Link href={href}>{t(`navbar.user.${id}`)}</Link>
Expand All @@ -60,7 +65,7 @@ const DesktopUserMenu = () => {
const MobileUserMenu = () => {
const t = useTranslations("Layout");
const { user, isAuthenticated } = useAuthUser();
const { logout } = useAuth();
const { logout } = useAuthService();

return (
<div className="border-t pt-4">
Expand Down
1 change: 0 additions & 1 deletion src/components/layout/WrapperLayout/constant.ts

This file was deleted.

33 changes: 0 additions & 33 deletions src/components/layout/WrapperLayout/index.tsx

This file was deleted.

63 changes: 14 additions & 49 deletions src/components/provider/AuthProvider/index.tsx
Original file line number Diff line number Diff line change
@@ -1,59 +1,24 @@
"use client";

import { createContext, ReactNode, useEffect, useState } from "react";
import { User, UserContextType } from "@/types";
import { decodeToken } from "@/lib/jwt";
// import { profileService } from "@/services/profile";
import { createContext, ReactNode, useState } from "react";
import { AuthJwtPayload, UserContextType } from "@/types";

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

export const AuthProvider = ({ children }: { children: ReactNode }) => {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
interface AuthProviderProps {
/**
* JWT Payload taken from cookie
* @default undefined
*/
payload?: AuthJwtPayload;
children: ReactNode;
}

const getUserProfile = async () => {
setIsLoading(true);
const token = await localStorage.getItem("accessToken");
if (!token) return;

const { isTokenExpired, username, role, email } = decodeToken(token);
console.log(decodeToken(token));
if (isTokenExpired) {
localStorage.removeItem("accessToken");
return;
}

try {
// const { data: user } = await profileService.getUserId();

setUser({
username: username,
email: email,
role: role,
// phone_number: phone_number || '',
});
} catch (err) {
console.error("Failed to fetch user profile:", err);
localStorage.removeItem("accessToken");
} finally {
setIsLoading(false);
}
};

useEffect(() => {
getUserProfile();
}, []);
export const AuthProvider = ({ payload, children }: AuthProviderProps) => {
const [user, setUser] = useState(payload || null);
const isAuthenticated = !!user;

return (
<UserContext.Provider
value={{
user,
setUser,
isLoading,
isAuthenticated: !!user,
}}
>
{children}
</UserContext.Provider>
<UserContext.Provider value={{ user, setUser, isAuthenticated, isLoading: false }}>{children}</UserContext.Provider>
);
};
4 changes: 2 additions & 2 deletions src/features/auth/forgot-password/ForgotPassPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ import { Button } from "@/components/ui/Button";
import { zodResolver } from "@hookform/resolvers/zod";
import { ForgotPasswordForm, forgotPasswordSchema } from "@/domains/Auth";
import { Form, FormControl, FormField, FormItem, FormMessage } from "@/components/ui/Form";
import { useAuth } from "../hooks/useAuth";
import { useAuthService } from "../hooks/useAuth";

const ForgotPassPage = () => {
const t = useTranslations("Auth.ForgotPassPage");
const { forgotPassword, isLoading } = useAuth();
const { forgotPassword, isLoading } = useAuthService();

const form = useForm<ForgotPasswordForm>({
resolver: zodResolver(forgotPasswordSchema),
Expand Down
Loading