Pada tutorial ini kita akan membahas tentang bagaimana cara membuat sebuah custom login dan register based on Next Auth bahkan menambahkan client side validation, middleware dan error handling.
Step 1: Setup Next Auth
Next Auth merupakan sebuah library open source yang membantumu dalam setup autentikasi secara cepat dan mudah pada sisi frontend hingga backend. Secara default next auth menyediakan template login dan register tersendiri, tetapi berdasarkan kebutuhan kali ini, kita akan melakukan kustomisasi baik dari sisi login maupun register.
Install next auth menggunakan command berikut:
npm i next-auth
npm i next-auth
Jika sudah buatlah sebuah file pada folder api sebagai next auth handler sebagai berikut:
import type { NextAuthOptions } from "next-auth";
import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
export const OPTIONS: NextAuthOptions = {
providers: [
CredentialsProvider({
name: "credentials",
credentials: {
username: {
label: "Username",
type: "text",
placeholder: "crusherblack",
},
password: { label: "Password", type: "password" },
},
}),
],
};
const handler = NextAuth(OPTIONS);
export { handler as GET, handler as POST };
import type { NextAuthOptions } from "next-auth";
import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
export const OPTIONS: NextAuthOptions = {
providers: [
CredentialsProvider({
name: "credentials",
credentials: {
username: {
label: "Username",
type: "text",
placeholder: "crusherblack",
},
password: { label: "Password", type: "password" },
},
}),
],
};
const handler = NextAuth(OPTIONS);
export { handler as GET, handler as POST };
Tambahkan SessionProvider agar kedepan kita bisa akses session dari sisi FE.
"use client";
import { ReactNode } from "react";
import { SessionProvider } from "next-auth/react";
import { StyleProvider } from "@ant-design/cssinjs";
import { store } from "@/store";
import { Provider as ReduxProvider } from "react-redux";
type Props = {
children: ReactNode,
};
const Provider = ({ children }: Props) => {
return (
<SessionProvider>
<ReduxProvider store={store}>
<StyleProvider hashPriority="high" ssrInline>
{children}
</StyleProvider>
</ReduxProvider>
</SessionProvider>
);
};
export default Provider;
"use client";
import { ReactNode } from "react";
import { SessionProvider } from "next-auth/react";
import { StyleProvider } from "@ant-design/cssinjs";
import { store } from "@/store";
import { Provider as ReduxProvider } from "react-redux";
type Props = {
children: ReactNode,
};
const Provider = ({ children }: Props) => {
return (
<SessionProvider>
<ReduxProvider store={store}>
<StyleProvider hashPriority="high" ssrInline>
{children}
</StyleProvider>
</ReduxProvider>
</SessionProvider>
);
};
export default Provider;
Tambahkan container pada app layout:
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import Provider from "@/provider";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "E-Commerce Tahu Coding",
description: "Modern E-Commerce with latest stack",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode,
}) {
return (
<html lang="en">
<body className={inter.className}>
<Provider>
<main className="mx-auto max-w-7xl px-8 min-h-screen">
{children}
</main>
</Provider>
</body>
</html>
);
}
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import Provider from "@/provider";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "E-Commerce Tahu Coding",
description: "Modern E-Commerce with latest stack",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode,
}) {
return (
<html lang="en">
<body className={inter.className}>
<Provider>
<main className="mx-auto max-w-7xl px-8 min-h-screen">
{children}
</main>
</Provider>
</body>
</html>
);
}
Step 2: Implementasi Register
Sebelum kita bisa melakukan login, tentunya kita perlu membuat sebuah page registrasi user terlebih dahulu agar dapat menambahkan user ke database, maka dari itu buatlah sebuah custom api untuk register pada folder api.
Tambahkan bcrypt sebagai library enkripsi/dekripsi password:
npm i bcrypt
npm i bcrypt
Tambahkan juga bcrypt typing menggunakan command berikut:
npm i --save-dev @types/bcrypt
npm i --save-dev @types/bcrypt
Tambahkan NEXTAUTH_URL & NEXTAUTH_SECRET pada .env, pastikan NEXTAUTH_SECRET value merupakan random string yang strong seperti 504-bit WPA Key.
# Environment variables declared in this file are automatically made available to Prisma.
# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema
# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
DATABASE_URL="YOUR_DB_URL"
NEXTAUTH_URL="http://localhost:3000"
NEXTAUTH_SECRET="RANDOM_STRING_FOR_NEXT_AUTH_SECRET"
# Environment variables declared in this file are automatically made available to Prisma.
# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema
# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
DATABASE_URL="YOUR_DB_URL"
NEXTAUTH_URL="http://localhost:3000"
NEXTAUTH_SECRET="RANDOM_STRING_FOR_NEXT_AUTH_SECRET"
Tambahkan axios agar kita bisa melakukan request ke BE nantinya:
npm i axios
npm i axios
Setelah berhasil menambahkan library buatlah sebuah file register\route.ts dan modifikasi sebagai berikut:
import { NextResponse } from "next/server";
import prisma from "@/lib/prisma";
import { hash } from "bcrypt";
const { json: jsonResponse } = NextResponse;
export const POST = async (request: Request) => {
try {
const { name, email, password } = await request.json();
//check if email already exist in database or not
const isUserExisted = await prisma.user.findUnique({
where: {
email,
},
});
//if so, then throw error email already exist
if (isUserExisted) {
return jsonResponse(
{
message: "Email Already Exist",
},
{
status: 409, //conflict status code
}
);
}
//if email don't exist continue creating user
const user = await prisma.user.create({
data: {
name,
email,
password: await hash(password, 10), //encrypt password
},
});
// exlude password from response, (note: you can customize the response object before send to client)
const { password: _, ...restUser } = user;
return jsonResponse(
{
message: "Successfully registered user",
data: restUser,
},
{
status: 201, //success created status code
}
);
} catch (_error) {
return jsonResponse(
{
message: "Server Error",
},
{
status: 500, //server error status code
}
);
}
};
import { NextResponse } from "next/server";
import prisma from "@/lib/prisma";
import { hash } from "bcrypt";
const { json: jsonResponse } = NextResponse;
export const POST = async (request: Request) => {
try {
const { name, email, password } = await request.json();
//check if email already exist in database or not
const isUserExisted = await prisma.user.findUnique({
where: {
email,
},
});
//if so, then throw error email already exist
if (isUserExisted) {
return jsonResponse(
{
message: "Email Already Exist",
},
{
status: 409, //conflict status code
}
);
}
//if email don't exist continue creating user
const user = await prisma.user.create({
data: {
name,
email,
password: await hash(password, 10), //encrypt password
},
});
// exlude password from response, (note: you can customize the response object before send to client)
const { password: _, ...restUser } = user;
return jsonResponse(
{
message: "Successfully registered user",
data: restUser,
},
{
status: 201, //success created status code
}
);
} catch (_error) {
return jsonResponse(
{
message: "Server Error",
},
{
status: 500, //server error status code
}
);
}
};
Code diatas bertanggung jawab dalam pembuatan user baru, setiap kali API ini dipanggil maka dia akan melakukan pengecekan apakah user sudah pernah terdaftar atau tidak based on email, jika user sudah terdaftar maka throw error, jika user belum terdaftar lanjutkan proses registrasi dan hash password serta exlude password setelah berhasil pada response.
Selanjutnya buatlah sebuah folder (auth) sebagai route placeholder yang membantu kita dalam identifikasi sebuah route tanpa perlu merubah struktur url.
Tambahkan page register pada folder (auth):
import Form from "./form";
export default async function Register() {
return (
<div className="flex min-h-screen items-center justify-center">
<div className="w-full md:w-8/12 lg:w-4/12 md:px-8 py-10">
<Form />
</div>
</div>
);
}
import Form from "./form";
export default async function Register() {
return (
<div className="flex min-h-screen items-center justify-center">
<div className="w-full md:w-8/12 lg:w-4/12 md:px-8 py-10">
<Form />
</div>
</div>
);
}
Bisa kita lihat, bahwa kita memanggil component Form dari file lain? alasannya karena next js 13 secara default merender page secara SSR, agar kita bisa melakukan interaksi dari sisi CSR maka dari itu kita buat sebuah component Form di folder yang sama.
Buatlah sebuah component baru pada folder yang sama yaitu Form:
"use client";
import { useState } from "react";
import { Alert, Button, Form, Input } from "antd";
import NextLink from "next/link";
import axios from "axios";
import { axiosErrorHandler } from "@/utils/errorHandling";
type FieldType = {
name: string;
email: string;
password: string;
passwordConfirmation: string;
};
const RegisterForm = () => {
const [loading, setLoading] = useState(false);
const [isRegisterSuccess, setIsRegisterSuccess] = useState(false);
const [error, setError] = useState("");
const [form] = Form.useForm();
const onFinish = async (values: FieldType) => {
try {
setError("");
setIsRegisterSuccess(false);
setLoading(true);
// do register to BE
await axios.post("/api/auth/register", values);
// if success show success alert & reset form
setIsRegisterSuccess(true);
form.resetFields();
} catch (error) {
//handle error based on axios instance or not
setError(axiosErrorHandler(error));
} finally {
setLoading(false);
}
};
return (
<Form
name="basic"
initialValues={{ remember: true }}
onFinish={onFinish}
autoComplete="off"
layout="vertical"
form={form}
>
{error && (
<Form.Item>
<Alert message={error} type="error" showIcon />
</Form.Item>
)}
{isRegisterSuccess && (
<Form.Item>
<Alert
message="Register Success Please Login!"
type="success"
showIcon
/>
</Form.Item>
)}
<p className="text-2xl font-bold">Welcome</p>
<p className="text-md mb-10 text-gray-500">
First, let's create your account
</p>
<Form.Item<FieldType>
label="Fullname"
name="name"
rules={[
{
required: true,
message: "Please input your Fullname!",
},
]}
>
<Input placeholder="Input your Fullname" size="large" />
</Form.Item>
<Form.Item<FieldType>
label="Email"
name="email"
rules={[
{
required: true,
message: "Please input your Email!",
type: "email",
},
]}
>
<Input placeholder="Input your email" size="large" />
</Form.Item>
<Form.Item<FieldType>
label="Password"
name="password"
rules={[
{
required: true,
min: 8,
max: 50,
message: "Please input your Password!",
},
]}
>
<Input.Password placeholder="Input your password" size="large" />
</Form.Item>
<Form.Item<FieldType>
label="Password Confirmation"
name="passwordConfirmation"
rules={[
{
required: true,
min: 8,
max: 50,
message: "Please confirm your Password!",
},
({ getFieldValue }) => ({
// add logic to make sure passwordConfirmation and password match each time user register
validator(_, value) {
if (!value || getFieldValue("password") === value) {
return Promise.resolve();
}
return Promise.reject(
new Error("The new password that you entered do not match!")
);
},
}),
]}
>
<Input.Password placeholder="Input your password" size="large" />
</Form.Item>
<Form.Item>
<Button
type="primary"
htmlType="submit"
loading={loading}
className="w-full mt-4"
size="large"
>
Register
</Button>
</Form.Item>
<p className="text-md">
Already have account?{" "}
<NextLink href="/login">
<span className="text-blue-600 hover:cursor-pointer">Login Now!</span>
</NextLink>
</p>
</Form>
);
};
export default RegisterForm;
"use client";
import { useState } from "react";
import { Alert, Button, Form, Input } from "antd";
import NextLink from "next/link";
import axios from "axios";
import { axiosErrorHandler } from "@/utils/errorHandling";
type FieldType = {
name: string;
email: string;
password: string;
passwordConfirmation: string;
};
const RegisterForm = () => {
const [loading, setLoading] = useState(false);
const [isRegisterSuccess, setIsRegisterSuccess] = useState(false);
const [error, setError] = useState("");
const [form] = Form.useForm();
const onFinish = async (values: FieldType) => {
try {
setError("");
setIsRegisterSuccess(false);
setLoading(true);
// do register to BE
await axios.post("/api/auth/register", values);
// if success show success alert & reset form
setIsRegisterSuccess(true);
form.resetFields();
} catch (error) {
//handle error based on axios instance or not
setError(axiosErrorHandler(error));
} finally {
setLoading(false);
}
};
return (
<Form
name="basic"
initialValues={{ remember: true }}
onFinish={onFinish}
autoComplete="off"
layout="vertical"
form={form}
>
{error && (
<Form.Item>
<Alert message={error} type="error" showIcon />
</Form.Item>
)}
{isRegisterSuccess && (
<Form.Item>
<Alert
message="Register Success Please Login!"
type="success"
showIcon
/>
</Form.Item>
)}
<p className="text-2xl font-bold">Welcome</p>
<p className="text-md mb-10 text-gray-500">
First, let's create your account
</p>
<Form.Item<FieldType>
label="Fullname"
name="name"
rules={[
{
required: true,
message: "Please input your Fullname!",
},
]}
>
<Input placeholder="Input your Fullname" size="large" />
</Form.Item>
<Form.Item<FieldType>
label="Email"
name="email"
rules={[
{
required: true,
message: "Please input your Email!",
type: "email",
},
]}
>
<Input placeholder="Input your email" size="large" />
</Form.Item>
<Form.Item<FieldType>
label="Password"
name="password"
rules={[
{
required: true,
min: 8,
max: 50,
message: "Please input your Password!",
},
]}
>
<Input.Password placeholder="Input your password" size="large" />
</Form.Item>
<Form.Item<FieldType>
label="Password Confirmation"
name="passwordConfirmation"
rules={[
{
required: true,
min: 8,
max: 50,
message: "Please confirm your Password!",
},
({ getFieldValue }) => ({
// add logic to make sure passwordConfirmation and password match each time user register
validator(_, value) {
if (!value || getFieldValue("password") === value) {
return Promise.resolve();
}
return Promise.reject(
new Error("The new password that you entered do not match!")
);
},
}),
]}
>
<Input.Password placeholder="Input your password" size="large" />
</Form.Item>
<Form.Item>
<Button
type="primary"
htmlType="submit"
loading={loading}
className="w-full mt-4"
size="large"
>
Register
</Button>
</Form.Item>
<p className="text-md">
Already have account?{" "}
<NextLink href="/login">
<span className="text-blue-600 hover:cursor-pointer">Login Now!</span>
</NextLink>
</p>
</Form>
);
};
export default RegisterForm;
Component ini bertanggung jawab dalam melakukan input data yang dibutuhkan serta validasi semua data apakah berupa email? apakah password sudah sesuai? sebelum API register yang sudah dibuat dipanggil, jika berhasil register tampilkan pesan suskes jika tidak tampilkan error.
Bisa kita lihat pada line 39 kita memanggil sebuah function axiosErrorHandler, maka dari itu mari buat function tersebut pada folder utils:
import axios from "axios";
type AxiosErrorResponse = {
//make sure to sync it with your BE if you want to use this in the future
message: string;
};
export const axiosErrorHandler = (error: unknown): string => {
//check if error is part of axios error
if (axios.isAxiosError(error) && error.response) {
return (error.response?.data as AxiosErrorResponse).message;
}
return "::CODE/RUNTIME ERROR =>" + error;
}
import axios from "axios";
type AxiosErrorResponse = {
//make sure to sync it with your BE if you want to use this in the future
message: string;
};
export const axiosErrorHandler = (error: unknown): string => {
//check if error is part of axios error
if (axios.isAxiosError(error) && error.response) {
return (error.response?.data as AxiosErrorResponse).message;
}
return "::CODE/RUNTIME ERROR =>" + error;
}
Function ini bertujuan untuk mengecek apakah error yang kita peroleh dari try and catch merupakan error axios request atau error biasa, jika sudah maka kamu dapat melakukan registrasi dan berikut tampilan page register.
![Register Page](/_next/image?url=%2Fstatic%2Fimages%2Ftutorial%2Fpart-2-tutorial-nextjs-13-typescript-ecommerce-cart-dengan-payment-gateway-antd-supabase-redux-toolkit-next-auth-postgre-sql-prisma%2Fregister-page.png&w=2048&q=100)
Step 3: Implementasi Login
Setelah berhasil setup next auth langkah berikutnya kita akan melakukan kustomisasi pada next auth api, panggil prisma client kemudian lakukan pengecekan terhadap email, jika sudah terdaftar lanjutkan daftar jika tidak throw error, berikut code-nya:
import prisma from "@/lib/prisma";
import type { NextAuthOptions } from "next-auth";
import { compare } from "bcrypt";
import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
export const OPTIONS: NextAuthOptions = {
providers: [
CredentialsProvider({
name: "credentials", //use this for custom login
credentials: {
username: {
label: "Username",
type: "text",
placeholder: "crusherblack",
},
password: { label: "Password", type: "password" },
},
async authorize(credentials) {
const loginErrorMessage = "Invalid email or password";
//check if email already exist or not
const isUserExisted = await prisma.user.findUnique({
where: {
email: credentials?.username,
},
});
//if not throw error
if (!isUserExisted) {
throw Error(loginErrorMessage);
}
//check if password that client input is them sama with hash password from db
if (
isUserExisted &&
credentials?.password &&
(await compare(credentials.password, isUserExisted.password))
) {
//if email and password valid continue
return isUserExisted;
}
//throw error if password not valid
throw Error(loginErrorMessage);
},
}),
],
pages: {
signIn: "/login", //tell next auth, this is url for custom login page
},
session: {
strategy: "jwt", //make sure to set it to jwt
},
jwt: {
secret: process.env.NEXTAUTH_SECRET, //fill with your own secret from .env
},
};
const handler = NextAuth(OPTIONS);
export { handler as GET, handler as POST };
import prisma from "@/lib/prisma";
import type { NextAuthOptions } from "next-auth";
import { compare } from "bcrypt";
import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
export const OPTIONS: NextAuthOptions = {
providers: [
CredentialsProvider({
name: "credentials", //use this for custom login
credentials: {
username: {
label: "Username",
type: "text",
placeholder: "crusherblack",
},
password: { label: "Password", type: "password" },
},
async authorize(credentials) {
const loginErrorMessage = "Invalid email or password";
//check if email already exist or not
const isUserExisted = await prisma.user.findUnique({
where: {
email: credentials?.username,
},
});
//if not throw error
if (!isUserExisted) {
throw Error(loginErrorMessage);
}
//check if password that client input is them sama with hash password from db
if (
isUserExisted &&
credentials?.password &&
(await compare(credentials.password, isUserExisted.password))
) {
//if email and password valid continue
return isUserExisted;
}
//throw error if password not valid
throw Error(loginErrorMessage);
},
}),
],
pages: {
signIn: "/login", //tell next auth, this is url for custom login page
},
session: {
strategy: "jwt", //make sure to set it to jwt
},
jwt: {
secret: process.env.NEXTAUTH_SECRET, //fill with your own secret from .env
},
};
const handler = NextAuth(OPTIONS);
export { handler as GET, handler as POST };
Singkat cerita, code diatas bertanggung jawab dalam melakukan pengecekan terhadap user yang sudah terdaftar atau tidak based on email, jika sudah terdaftar maka code akan melakukan komparasi terhadap hash password yang ada di db dengan plain password yang kita kirim dari FE menggunakan bcrypt, jika sesuai maka kita dapat login jika tidak throw error.
Terdapat beberapa config yang kita lakukan disini, diataranya adalah:
- pages - signIn - "/login" => bertujuan untuk memberi tahu next auth url dari custom login page
- session - strategy - "jwt" -> strategi auth yang berbasis token yang secure.
- jwt - secret - process.env.NEXTAUTH_SECRET => merupakan secret jwt key yang digunakan next auth ketika membuat token (pastikan kamu menggunakan secret key yang baik seperti 504-bit WPA Key)
Agar dapat menggunakan handle diatas, kita akan membuat sebuah page login baru pada folder app.
Jika sudah buatlah file route.ts didalam app/(auth)/login dan tambahkan code berikut:
import Form from "./form";
export default async function Login() {
return (
<div className="flex min-h-screen items-center justify-center">
<div className="w-full md:w-8/12 lg:w-4/12 md:px-8 py-10">
<Form />
</div>
</div>
);
}
import Form from "./form";
export default async function Login() {
return (
<div className="flex min-h-screen items-center justify-center">
<div className="w-full md:w-8/12 lg:w-4/12 md:px-8 py-10">
<Form />
</div>
</div>
);
}
Buatlah sebuah file form.tsx kemudian tambahkan code berikut:
"use client";
import { useState } from "react";
import { signIn } from "next-auth/react";
import { useSearchParams, useRouter } from "next/navigation";
import { Alert, Button, Form, Input } from "antd";
import NextLink from "next/link";
type FieldType = {
email: string;
password: string;
};
const LoginForm = () => {
const router = useRouter();
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const searchParams = useSearchParams();
const callbackUrl = searchParams.get("callbackUrl") || "/dashboard";
const onFinish = async (values: FieldType) => {
try {
setLoading(true);
//do login to next auth based on email and password
const res = await signIn("credentials", {
redirect: false,
username: values.email,
password: values.password,
callbackUrl,
});
//if there is no error, redirect to dashboard or previous page
if (!res?.error) {
router.push(callbackUrl);
} else {
throw Error(res?.error);
}
} catch (error: any) {
if (error) setError(error?.message);
} finally {
setLoading(false);
}
};
return (
<Form
name="basic"
initialValues={{ remember: true }}
onFinish={onFinish}
autoComplete="off"
layout="vertical"
>
{error && (
<Form.Item>
<Alert message={error} type="error" showIcon />
</Form.Item>
)}
<p className="text-2xl font-bold">Hello, Welcome Back</p>
<p className="text-md mb-10 text-gray-500">
Happy to see you again, login here
</p>
<Form.Item<FieldType>
label="Email"
name="email"
rules={[
{
required: true,
message: "Please input your Email!",
type: "email",
},
]}
>
<Input placeholder="Input your email" size="large" />
</Form.Item>
<Form.Item<FieldType>
label="Password"
name="password"
rules={[
{
required: true,
min: 8,
max: 50,
message: "Please input your Password!",
},
]}
>
<Input.Password placeholder="Input your password" size="large" />
</Form.Item>
<Form.Item>
<Button
type="primary"
htmlType="submit"
loading={loading}
className="w-full mt-4"
size="large"
>
Login
</Button>
</Form.Item>
<p className="text-md">
Don't have account?{" "}
<NextLink href="/register">
<span className="text-blue-600 hover:cursor-pointer">
Register Now!
</span>
</NextLink>
</p>
</Form>
);
};
export default LoginForm;
"use client";
import { useState } from "react";
import { signIn } from "next-auth/react";
import { useSearchParams, useRouter } from "next/navigation";
import { Alert, Button, Form, Input } from "antd";
import NextLink from "next/link";
type FieldType = {
email: string;
password: string;
};
const LoginForm = () => {
const router = useRouter();
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const searchParams = useSearchParams();
const callbackUrl = searchParams.get("callbackUrl") || "/dashboard";
const onFinish = async (values: FieldType) => {
try {
setLoading(true);
//do login to next auth based on email and password
const res = await signIn("credentials", {
redirect: false,
username: values.email,
password: values.password,
callbackUrl,
});
//if there is no error, redirect to dashboard or previous page
if (!res?.error) {
router.push(callbackUrl);
} else {
throw Error(res?.error);
}
} catch (error: any) {
if (error) setError(error?.message);
} finally {
setLoading(false);
}
};
return (
<Form
name="basic"
initialValues={{ remember: true }}
onFinish={onFinish}
autoComplete="off"
layout="vertical"
>
{error && (
<Form.Item>
<Alert message={error} type="error" showIcon />
</Form.Item>
)}
<p className="text-2xl font-bold">Hello, Welcome Back</p>
<p className="text-md mb-10 text-gray-500">
Happy to see you again, login here
</p>
<Form.Item<FieldType>
label="Email"
name="email"
rules={[
{
required: true,
message: "Please input your Email!",
type: "email",
},
]}
>
<Input placeholder="Input your email" size="large" />
</Form.Item>
<Form.Item<FieldType>
label="Password"
name="password"
rules={[
{
required: true,
min: 8,
max: 50,
message: "Please input your Password!",
},
]}
>
<Input.Password placeholder="Input your password" size="large" />
</Form.Item>
<Form.Item>
<Button
type="primary"
htmlType="submit"
loading={loading}
className="w-full mt-4"
size="large"
>
Login
</Button>
</Form.Item>
<p className="text-md">
Don't have account?{" "}
<NextLink href="/register">
<span className="text-blue-600 hover:cursor-pointer">
Register Now!
</span>
</NextLink>
</p>
</Form>
);
};
export default LoginForm;
Pada line 22 kita bisa melihat ada sebuah variable yang bertanggung jawab dalam menghandle route redirect, variable ini berisi route sebelum kita menuju login dan route default setelah kita login. Apa benefitnya?, jika kamu mengalami expire session pada menu yang sudah diproteksi dan melanjutkan login ulang, maka aplikasi akan redirect ke page sebelumnya ketimbang redirect ke default route, hal ini meningkatkan user experience dan mempermudamu dalam penggunaan aplikasi.
Jika sudah maka tampilan dari login form kita sebagai berikut:
![Login Page](/_next/image?url=%2Fstatic%2Fimages%2Ftutorial%2Fpart-2-tutorial-nextjs-13-typescript-ecommerce-cart-dengan-payment-gateway-antd-supabase-redux-toolkit-next-auth-postgre-sql-prisma%2Flogin-page.png&w=2048&q=100)
Kamu dapat melakukan login tetapi karena route dahsboard belum kita buat maka dari itu tambahkan dashboard pada app:
import { getServerSession } from "next-auth";
import React from "react";
import { OPTIONS } from "../api/auth/[...nextauth]/route";
const Dashboard = async () => {
const session = await getServerSession(OPTIONS);
return <div>Welcome: {session?.user?.email}</div>;
};
export default Dashboard;
import { getServerSession } from "next-auth";
import React from "react";
import { OPTIONS } from "../api/auth/[...nextauth]/route";
const Dashboard = async () => {
const session = await getServerSession(OPTIONS);
return <div>Welcome: {session?.user?.email}</div>;
};
export default Dashboard;
Jika sudah kamu sudah mencoba login menggunakan user yang teregistrasi.
Step 4: Tambahkan middleware
Meskipun kita sudah memiliki fungsi login tetapi dashboard dapat diakses tanpa perlu login, maka dari itu tambahkan middleware yang bertanggung jawab dalam proteksi route-route pada aplikasi:
import { getToken } from "next-auth/jwt";
import { NextRequest, NextResponse } from "next/server";
//this is custom middleware you may improve it as you like
export default async function middleware(req: NextRequest) {
// Get the pathname of the request (e.g. /, /protected)
const path = req.nextUrl.pathname;
// If it's the root path, just render it
if (path === "/") {
return NextResponse.next();
}
//decript jwt based on NEXTAUTH_SECRET
const session = await getToken({
req,
secret: process.env.NEXTAUTH_SECRET,
});
const isProtected = path.includes("/dashboard");
//if jwt token valid then continue, otherwise redirect to login
if (!session && isProtected) {
return NextResponse.redirect(new URL("/login", req.url));
} else if (session && (path === "/login" || path === "/register")) {
return NextResponse.redirect(new URL("/dashboard", req.url));
}
return NextResponse.next();
}
import { getToken } from "next-auth/jwt";
import { NextRequest, NextResponse } from "next/server";
//this is custom middleware you may improve it as you like
export default async function middleware(req: NextRequest) {
// Get the pathname of the request (e.g. /, /protected)
const path = req.nextUrl.pathname;
// If it's the root path, just render it
if (path === "/") {
return NextResponse.next();
}
//decript jwt based on NEXTAUTH_SECRET
const session = await getToken({
req,
secret: process.env.NEXTAUTH_SECRET,
});
const isProtected = path.includes("/dashboard");
//if jwt token valid then continue, otherwise redirect to login
if (!session && isProtected) {
return NextResponse.redirect(new URL("/login", req.url));
} else if (session && (path === "/login" || path === "/register")) {
return NextResponse.redirect(new URL("/dashboard", req.url));
}
return NextResponse.next();
}
Sekarang coba clear semua cookies kemudian paksa masuk pada page dashboard.
Tidak bisa bukan? Selamat middlewaremu berjalan dengan baik, kedepan kita akan mengembangkan middleware ini untuk handling protected route based on user role.
Step 5: Tambahkan fungsi Logout
Pada page dashboard tambahkan component LogoutButton
import { getServerSession } from "next-auth";
import React from "react";
import { OPTIONS } from "../api/auth/[...nextauth]/route";
import LogoutButton from "./logoutButton";
const Dashboard = async () => {
const session = await getServerSession(OPTIONS);
return (
<div>
Welcome: {session?.user?.email} <LogoutButton />
</div>
);
};
export default Dashboard;
import { getServerSession } from "next-auth";
import React from "react";
import { OPTIONS } from "../api/auth/[...nextauth]/route";
import LogoutButton from "./logoutButton";
const Dashboard = async () => {
const session = await getServerSession(OPTIONS);
return (
<div>
Welcome: {session?.user?.email} <LogoutButton />
</div>
);
};
export default Dashboard;
Lalu buat component tersebut:
"use client";
import { Button } from "antd";
import { signOut } from "next-auth/react";
import React from "react";
const LogoutButton = () => {
const handleLogout = () => {
signOut();
};
return (
<Button type="primary" onClick={handleLogout}>
Logout
</Button>
);
};
export default LogoutButton;
"use client";
import { Button } from "antd";
import { signOut } from "next-auth/react";
import React from "react";
const LogoutButton = () => {
const handleLogout = () => {
signOut();
};
return (
<Button type="primary" onClick={handleLogout}>
Logout
</Button>
);
};
export default LogoutButton;
Selamat kamu sudah menyelesaikan tutorial kali ini, kedepan kita akan membuat layout dashboard dan landing.
Code untuk tutorial kali ini link repo, jangan lupa star di repo ya!
Stay tune dan jangan lupa share & support Tahu Coding, Terima Kasih!