iT邦幫忙

2023 iThome 鐵人賽

DAY 21
0
自我挑戰組

模仿知名網站的外觀系列 第 21

【Day21】模仿知名網站的外觀 X(8) 完成註冊功能

  • 分享至 

  • xImage
  •  

上回我們完成資料庫的設定了,接下來透過Prisma的幫助在前端專案讀取和修改MongoDB的資料。

為了做到這一點,需要安裝套件,在終端機輸入:

npm i @prisma/client

在根目錄創建libs資料夾,在libs資料夾下,新增prismadb.ts,導入和使用 Prisma Client。

import { PrismaClient } from "@prisma/client";

declare global {
    var prisma: PrismaClient | undefined
}

const client = globalThis.prisma || new PrismaClient()
if(process.env.NODE_ENV !== 'production') globalThis.prisma = client;

export default client;

接著處理登入和註冊的實作,我們會使用到一些套件,因此需要安裝。

npm i bcrypt
npm i -d @types/bcrypt
npm i next-auth
npm i @next-auth/prisma-adapter
npm i swr
npm i axios
npm i react-hot-toast

在pages新增api資料夾,在api資料夾下,新增[...nextauth].ts。

import bcrypt from "bcrypt";
import NextAuth, { AuthOptions } from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import { PrismaAdapter } from "@next-auth/prisma-adapter";

import prisma from "@/libs/prismadb";

export const authOptions: AuthOptions = {
	adapter: PrismaAdapter(prisma),
	providers: [
		CredentialsProvider({
			name: "credentials",
			credentials: {
				email: { label: "email", type: "text" },
				password: { label: "password", type: "password" },
			},
			async authorize(credentials) {
				if (!credentials?.email || !credentials?.password) {
					throw new Error("Invalid credentials");
				}

				const user = await prisma.user.findUnique({
					where: {
						email: credentials.email,
					},
				});

				if (!user || !user?.hashedPassword) {
					throw new Error("Invalid credentials");
				}

				const isCorrectPassword = await bcrypt.compare(
					credentials.password,
					user.hashedPassword
				);

				if (!isCorrectPassword) {
					throw new Error("Invalid credentials");
				}

				return user;
			},
		}),
	],
	debug: process.env.NODE_ENV === "development",
	session: {
		strategy: "jwt",
	},
	jwt: {
		secret: process.env.NEXTAUTH_JWT_SECRET,
	},
	secret: process.env.NEXTAUTH_SECRET,
};

export default NextAuth(authOptions);

我們使用CredentialsProvider,代表使用電子信箱和密碼進行登入,authorize是處理登入的部分,我們先檢查電子信箱和密碼是不是空的,如果是空的就顯示錯誤,然後在資料庫中根據電子信箱尋找用戶,如果沒找到或是找到的用戶沒有密碼,彈出錯誤,最後比較加密後的密碼和資料庫中用戶的密碼相不相同,不同就無法登入,相同就傳回用戶資料。

修改.env,NEXTAUTH_JWT_SECRET和NEXTAUTH_SECRET可以在終端機輸入以下指令獲得,產生的內容是隨機的,不一樣是正常的。

openssl rand -base64 32
# 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="mongodb+srv://username:password@cluster0.fzdzygu.mongodb.net/x"
NEXTAUTH_JWT_SECRET="QGhYJWITazMvpgbVxEBnK0ohpcrFGRxvxyGF7pCyZ1E="
NEXTAUTH_SECRET="sng6IW8wKSO/4I4MBm6Mgjat4BKQiYd7vrmUrtFBP54="

在api資料夾下,新增register.ts,用來註冊新用戶。

import bcrypt from "bcrypt";
import { NextApiRequest, NextApiResponse } from "next";
import prisma from "@/libs/prismadb";

export default async function handle(req: NextApiRequest, res: NextApiResponse) {
    if(req.method !== 'POST'){
        return res.status(405).end();
    }

    try {
        const {email, username, name, password} = req.body;

        const hashedPassword = await bcrypt.hash(password, 12);

        const user = await prisma.user.create({
            data: {
                email,
                username,
                name,
                hashedPassword
            }
        });

        return res.status(200).json(user);
    } catch(error) {
        console.log(error);
        return res.status(400).end();
    }
}

在libs資料夾下,新增serverAuth.ts,驗證有沒有登入。

import { NextApiRequest, NextApiResponse } from 'next';

import prisma from '@/libs/prismadb';
import { authOptions } from '@/pages/api/auth/[...nextauth]';
import { getServerSession } from 'next-auth';

const serverAuth = async (req: NextApiRequest, res: NextApiResponse) => {
  const session = await getServerSession(req, res, authOptions);

  if (!session?.user?.email) {
    throw new Error('Not signed in');
  } 

  const currentUser = await prisma.user.findUnique({
    where: {
      email: session.user.email,
    }
  });

  if (!currentUser) {
    throw new Error('Not signed in');
  }

  return { currentUser };
};

export default serverAuth;

在api資料夾下,新增current.ts,從資料庫取得目前登入的用戶資訊。

import serverAuth from "@/libs/serverAuth";
import { NextApiRequest, NextApiResponse } from "next";

export default async function handler(
	req: NextApiRequest,
	res: NextApiResponse
) {
	if (req.method !== "GET") {
		return res.status(405).end();
	}

	try {
		const { currentUser } = await serverAuth(req, res);
		return res.status(200).json(currentUser);
	} catch (error) {
		console.log(error);
		return res.status(400).end();
	}
}

在libs資料夾下,新增fetcher.ts,取得特定網址的資料後回傳。

import axios from "axios";

const fetcher = (url: string) => axios.get(url).then((res) => res.data);

export default fetcher

在Hooks資料夾下,新增useCurrentUser.ts,用來取得目前登入的用戶資訊,會先從快取中取得資料,如果資料太舊了,取得新資料的同時也更新快取中的內容。

import useSWR, { mutate } from "swr";
import fetcher from "@/libs/fetcher";

const useCurrentUser = () => {
const {data, error, isLoading, mutate} = useSWR("/api/current", fetcher)

  return {
    data,
    error,
    isLoading,
    mutate
  }
}

export default useCurrentUser

修改RegisterModal.tsx,實現註冊功能,註冊成功會跳出一個寫著【Account created】的小視窗,如果有錯誤的話則是會彈出【Something went wrong】。

'use client'

import useLoginModal from "@/Hooks/useLoginModal";
import { useCallback, useState } from "react";
import Input from "../Input";
import Modal from "../Modal";
import useRegisterModal from "@/Hooks/useRegisterModal";
import axios from "axios";
import { toast } from "react-hot-toast";
import { signIn } from "next-auth/react";

const RegisterModal = () => {
	const loginModal = useLoginModal();
    const registerModal = useRegisterModal();

	const [email, setEmail] = useState("");
	const [password, setPassword] = useState("");
    const [name, setName] = useState("");
    const [username, setUsername] = useState("");
	const [isLoading, setIsLoading] = useState(false);

    const onToggle = useCallback(() => {
        if(isLoading){
            return;
        }

        registerModal.onClose();
        loginModal.onOpen();
    }, [isLoading, registerModal, loginModal]);

	const onSubmit = useCallback(async () => {
		try {
			setIsLoading(true);
			await axios.post("/api/register", {
				email,
				password,
				username,
				name
			})

			toast.success('Account created.');

			signIn('credentials', {
				email,
				password
			});

			registerModal.onClose();
		} catch (error) {
			console.log(error);
			toast.error('Something went wrong');
		} finally {
			setIsLoading(false);
		}
	}, [registerModal, email, password, username, name]);

	const bodyContent = (
		<div className="flex flex-col gap-4">
			<Input
				placeholder="Email"
				onChange={(e) => setEmail(e.target.value)}
				value={email}
				disabled={isLoading}
			/>
            <Input
				placeholder="Name"
				onChange={(e) => setName(e.target.value)}
				value={name}
				disabled={isLoading}
			/>
            <Input
				placeholder="Username"
				onChange={(e) => setUsername(e.target.value)}
				value={username}
				disabled={isLoading}
			/>
			<Input
				placeholder="Password"
				onChange={(e) => setPassword(e.target.value)}
				value={password}
				disabled={isLoading}
			/>
		</div>
	);

    const footerContent = (
        <div className="text-neutral-400 text-center mt-4">
            <p>
                Already have an account?
                <span onClick={onToggle} className="text-white cursor-pointer hover:underline">
                    Sign in
                </span>
            </p>
        </div>
    )

	return (
		<Modal
			disabled={isLoading}
			isOpen={registerModal.isOpen}
			title="Create an account"
			actionLabel="Register"
			onClose={registerModal.onClose}
			onSubmit={onSubmit}
			body={bodyContent}
            footer={footerContent}
		/>
	);
};

export default RegisterModal;

啟動專案,按下Tweet按鈕找到註冊頁面,輸入資料後按下Register,成功的話,來到MongoDB的BrowserCollections,選擇User欄位,會看到類似以下的內容。

_id:64f935422f6e89bc0afa8ae4
name:"a"
username:"b"
email:"a@a.com"
hashedPassword:"$2b$12$8LJfbCro9lRtYl1iidKa5e5sqTOG1KiW1Zj4UxGJU1i9sOZftGoim"
createdAt:2023-09-24T02:28:17.788+00:00
updatedAt:2023-09-24T02:28:17.788+00:00

上一篇
【Day20】模仿知名網站的外觀 X(7) Prisma連接MongoDB資料庫
下一篇
【Day22】模仿知名網站的外觀 X(9) 登入實作與推薦追蹤區塊
系列文
模仿知名網站的外觀30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言