上回我們完成資料庫的設定了,接下來透過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