Skip to content

Commit

Permalink
feat: refine login and implement middleware
Browse files Browse the repository at this point in the history
  • Loading branch information
izzuzantyaf committed Dec 31, 2024
1 parent d65998f commit 3c12cc8
Show file tree
Hide file tree
Showing 13 changed files with 203 additions and 10 deletions.
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Application variables
NEXT_PUBLIC_APP_ENV=production
NEXT_PUBLIC_FISDASWEB_API_URL=http://localhost:8080
NEXT_PUBLIC_JWT_SECRET=supersecretkey
ACCESS_TOKEN_SECRET=supersecretkey
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@
"@mantine/core": "^7.15.2",
"@mantine/form": "^7.15.2",
"@mantine/hooks": "^7.15.2",
"@mantine/notifications": "^7.15.2",
"@mantine/nprogress": "^7.15.2",
"@tanstack/react-query": "^5.62.11",
"axios": "^1.7.9",
"jose": "^5.9.6",
"next": "15.1.3",
"react": "^19.0.0",
"react-dom": "^19.0.0"
Expand Down
51 changes: 51 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { ColorSchemeScript } from "@mantine/core";
import { MantineProvider, TanstackQueryProvider } from "@/app/providers";
import "@mantine/notifications/styles.css";

const geistSans = Geist({
variable: "--font-geist-sans",
Expand Down
30 changes: 28 additions & 2 deletions src/app/login/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"use client";
import { useLogin } from "@/auth";
import { LoginDto, useLogin } from "@/auth";
import {
Button,
Container,
Expand All @@ -9,6 +9,10 @@ import {
Title,
} from "@mantine/core";
import { useForm } from "@mantine/form";
import { useRouter } from "next/navigation";
import { notifications } from "@mantine/notifications";
import React from "react";
import { AxiosError } from "axios";

export default function LoginPage() {
const form = useForm({
Expand All @@ -21,6 +25,28 @@ export default function LoginPage() {

const login = useLogin();

const router = useRouter();

const loginErrorNotificationId = React.useRef("login-error");

async function handleLogin(data: LoginDto) {
try {
await login.mutateAsync(data);

router.push("/");
} catch (error) {
if (error instanceof AxiosError) {
notifications.show({
id: loginErrorNotificationId.current,
title: "Login Failed",
message: error.response?.data?.message,
position: "bottom-center",
color: "red",
});
}
}
}

return (
<>
<Container size={420} my={40}>
Expand All @@ -29,7 +55,7 @@ export default function LoginPage() {
<Paper withBorder shadow="md" p={30} mt={30} radius="md">
<form
onSubmit={form.onSubmit(values => {
login.mutate(values);
handleLogin(values);
})}
>
<TextInput
Expand Down
31 changes: 30 additions & 1 deletion src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,32 @@
"use client";
import { useLogout } from "@/auth";
import { Button } from "@mantine/core";
import { useRouter } from "next/navigation";

export default function Home() {
return <div>Home page</div>;
const logout = useLogout();

const router = useRouter();

function handleLogout() {
logout.mutate();

router.push("/login");
}

return (
<>
<div>Home page</div>

<Button
color="red"
variant="light"
onClick={() => {
handleLogout();
}}
>
Log out
</Button>
</>
);
}
8 changes: 7 additions & 1 deletion src/app/providers/mantine.provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
MantineProvider as _MantineProvider,
createTheme,
} from "@mantine/core";
import { Notifications } from "@mantine/notifications";

const theme = createTheme({
defaultRadius: "md",
Expand Down Expand Up @@ -30,5 +31,10 @@ export function MantineProvider({
}: Readonly<{
children: React.ReactNode;
}>) {
return <_MantineProvider theme={theme}>{children}</_MantineProvider>;
return (
<_MantineProvider theme={theme}>
<Notifications />
{children}
</_MantineProvider>
);
}
24 changes: 24 additions & 0 deletions src/auth/helpers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { stringToUint8Array } from "@/common";
import { NextRequest } from "next/server";
import * as jose from "jose";

export function isProtectedPath(path: string) {
const PROTECTED_PATHS = ["/"];

return PROTECTED_PATHS.includes(path);
}

export async function verifyAccessTokenFromRequest(request: NextRequest) {
if (!request.cookies.has("access_token")) {
return false;
}

const key = stringToUint8Array(process.env.ACCESS_TOKEN_SECRET || "");

const isTokenVerified = await jose.jwtVerify(
request.cookies.get("access_token")?.value || "",
key
);

return isTokenVerified;
}
20 changes: 15 additions & 5 deletions src/auth/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import { LoginDto, LoginResponse } from "@/auth/types";
import { setCookie } from "@/common";
import { clearCookie, setCookie } from "@/common";
import { apiClient } from "@/common/helpers";
import { useMutation } from "@tanstack/react-query";
import axios from "axios";

export function useLogin() {
const mutation = useMutation({
mutationFn: (data: LoginDto) =>
axios.post<LoginResponse>("/auth/admin/login", data, {
baseURL: process.env.NEXT_PUBLIC_FISDASWEB_API_URL,
}),
apiClient.post<LoginResponse>("/auth/admin/login", data),
onSuccess(data) {
setCookie({
name: "access_token",
Expand All @@ -20,3 +18,15 @@ export function useLogin() {

return mutation;
}

export function useLogout() {
const mutation = useMutation({
mutationFn: () => {
clearCookie("access_token");

return Promise.resolve();
},
});

return mutation;
}
1 change: 1 addition & 0 deletions src/auth/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./hooks";
export * from "./types";
export * from "./helpers";
5 changes: 5 additions & 0 deletions src/common/helpers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import axios from "axios";

export const apiClient = axios.create({
baseURL: process.env.NEXT_PUBLIC_FISDASWEB_API_URL,
});
12 changes: 12 additions & 0 deletions src/common/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,15 @@ export function setCookie(props: {
secure ? "Secure" : ""
}`;
}

export function clearCookie(name: string) {
setCookie({
name,
value: "",
maxAge: 0,
});
}

export function stringToUint8Array(value: string) {
return new TextEncoder().encode(value);
}
26 changes: 26 additions & 0 deletions src/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { isProtectedPath, verifyAccessTokenFromRequest } from "@/auth";
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

// This function can be marked `async` if using `await` inside
export async function middleware(request: NextRequest) {
const path = request.nextUrl.pathname;

const isAuthenticated = await verifyAccessTokenFromRequest(request);

const LOGIN_PATH = "/login";

if (isProtectedPath(path)) {
if (isAuthenticated) {
return NextResponse.next();
}

return NextResponse.redirect(new URL(LOGIN_PATH, request.url));
}

if (path === LOGIN_PATH && isAuthenticated) {
return NextResponse.redirect(new URL("/", request.url));
}

return NextResponse.next();
}

0 comments on commit 3c12cc8

Please sign in to comment.