在本文中,我们将学习如何使用 Next.js、 Prisma、 Postgres 和 Fastify 构建一个 Full-stack 应用程序。
在本文中,我们将学习如何使用 Next.js、 Prisma、 Postgres 和 Fastify 构建一个 Full-stack 应用程序。我们将建立一个考勤管理演示应用程序,管理员工的考勤。这个应用程序的流程很简单: 管理用户登录,创建当天的出勤表,然后每个员工在出勤表上进出。
Next.Js 是一个灵活的 React 框架,它为您提供了创建快速 Web 应用程序的构建块。它通常被称为全栈 React 框架,因为它可以让前端和后端应用程序在相同的代码基上使用无服务器函数来实现这一点。
Prisma 是一个开源的、 Node.js 和 Typecript ORM,它极大地简化了 SQL 数据库的数据建模、迁移和数据访问。在撰写本文时,Prisma 支持以下数据库管理系统: PostgreSQL、 MySQL、 MariaDB、 SQLite、 AWS Aurora、 Microsoft SQL Server、 Azure SQL 和 MongoDB。您可能还希望单击此处查看所有受支持的数据库管理系统的列表。
Postgres 也被称为 PostgreSQL,它是一个免费的开源关联式资料库管理系统。它是 SQL 语言的超集,具有许多特性,使开发人员能够安全地存储和扩展复杂的数据工作负载。
本教程是一个实践演示教程。因此,最好在你的电脑上安装以下软件:
这个教程的代码可以在 Github 上找到,所以你可以克隆它,然后跟着学习。
https://github.com/Claradev32/attendance
让我们从设置 Next.js 应用程序开始。
Shell
npx create-next-App@latest
等待安装完成,然后运行下面的命令来安装我们的依赖项。
Shell
yarn add fastify fastify-nextjs iron-session @prisma/clientbryarn add prisma nodemon --dev
等待安装完成。
默认情况下,Next.js 不使用 Fastify 作为其服务器。要使用 Fastify 为我们的 Next.js 应用程序提供服务,请编辑 package.json 文件中的脚本字段,其代码片段如下所示。
JSON
scripts": {
"dev": "nodemon server.js",
"build": "next build",
"start": "next start",
"lint": "next lint"
}
现在让我们创建一个 server.js 文件。这个文件是我们的应用程序的入口点,然后我们添加了一个需求(‘ fast tify-nextjs’)来包含一个插件,这个插件在 Fastify 公开了 Next.js API 来处理渲染。
打开 server.js 文件,并添加以下代码段:
const fastify = require('fastify')()
async function noOpParser(req, payload) {
return payload;
}
fastify.register(require('fastify-nextjs')).after(() => {
fastify.addContentTypeParser('text/plain', noOpParser);
fastify.addContentTypeParser('application/json', noOpParser);
fastify.next('/*')
fastify.next('/api/*', { method: 'ALL' });
})
fastify.listen(3000, err => {
if (err) throw err
console.log('Server listening on <http://localhost:3000>')
})
在上面的代码片段中,我们使用了 fast-nextjs 插件,它在 Fastify 公开了处理呈现的 Next.js API。然后,我们使用 noopParser 函数解析传入的请求,这使得请求体可用于我们的 Next.js API 路由处理程序,我们使用[ fasttify.next ](< http://fastify.next > 命令)为我们的应用定义两个路由。然后我们创建 Fastify 服务器,让它监听端口3000。
现在继续并使用纱线 dev 命令运行应用程序: 应用程序将在 localhost: 3000上运行。
首先,运行以下命令来获得一个基本的 Prisma 设置:
Shell
npx prisma init
上面的命令将创建一个包含 schema.Prisma 文件的 Prisma 目录。这是您的主 Prisma 配置文件,它将包含您的数据库模式。还有。Env 文件将被添加到项目的根目录中。打开。Env 文件,并用 PostgreSQL 数据库的连接 URL 替换虚拟连接 URL。
将 prisma/schema.prisma 文件中的代码替换为以下内容:
Properties files 属性文件
datasource db {
url = env("DATABASE_URL")
provider="postgresql"
}
generator client {
provider = "prisma-client-js"
}
model User {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
email String @unique
name String
password String
role Role @default(EMPLOYEE)
attendance Attendance[]
AttendanceSheet AttendanceSheet[]
}
model AttendanceSheet {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
createdBy User? @relation(fields: [userId], references: [id])
userId Int?
}
model Attendance {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
signIn Boolean @default(true)
signOut Boolean
signInTime DateTime @default(now())
signOutTime DateTime
user User? @relation(fields: [userId], references: [id])
userId Int?
}
enum Role {
EMPLOYEE
ADMIN
}
在上面的代码片段中,我们创建了 User、 AttendanceSheet 和 Attendance Model,定义了每个模型之间的关系。
接下来,在数据库中创建这些表:
Shell
npx prisma db push
在运行上面的命令之后,您应该可以在终端中看到如下截图所示的输出:
完成 Prisma 设置后,让我们创建三个实用函数,它们将在我们的应用程序中不时使用。
打开 lib/parseBody.js 文件并添加以下代码片段:
JAVAScript
export const parseBody = (body) => {
if (typeof body === "string") return JSON.parse(body)
return body
}
打开 lib/request.js 文件并添加以下代码片段。
export const sessionCookie = () => {
return ({
cookieName: "auth",
password: process.env.SESSION_PASSWORD,
// secure: true should be used in production (HTTPS) but can't be used in development (HTTP)
cookieOptions: {
secure: process.env.NODE_ENV === "production",
},
})
}
打开/lib/request.js 文件并添加以下代码片段。此函数返回铁会话铁会话的会话属性对象。
JavaScript
export const sessionCookie = () => {
return ({
cookieName: "auth",
password: process.env.SESSION_PASSWORD,
// secure: true should be used in production (HTTPS) but can't be used in development (HTTP)
cookieOptions: {
secure: process.env.NODE_ENV === "production",
},
})
}
接下来,将 SESSION_PASSWORD 添加到. env 文件: 它应该是一个至少有32个字符的字符串。
完成我们的实用程序功能后,让我们给应用程序添加一些样式。我们正在为这个应用程序使用 css 模块,所以打开 style/Home.modules.CSS 文件并添加以下代码片段:
CSS
.container {
padding: 0 2rem;
}
.man {
min-height: 100vh;
padding: 4rem 0;
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.login {
width: 450px;
}
.login input {
width: 100%;
height: 50px;
margin: 4px;
}
.login button {
width: 100%;
height: 50px;
margin: 4px;
}
.dashboard {
display: grid;
grid-template-columns: 3fr 9fr;
grid-template-rows: 1fr;
grid-column-gap: 0px;
grid-row-gap: 0px;
height: calc(100vh - 60px);
}
.navbar {
height: 60px;
background-color: black;
}
设计完成后,让我们创建侧边栏组件来帮助我们在应用程序仪表板上导航到不同的页面。打开 Component/SideBar.js 文件,并粘贴下面的代码片段。
JavaScript
import Link from 'next/link'
import { useRouter } from 'next/router'
import styles from '../styles/SideBar.module.css'
const SideBar = () => {
const router = useRouter()
const logout = async () => {
try {
const response = await fetch('/api/logout', {
method: 'GET',
credentials: 'same-origin',
});
if(response.status === 200) router.push('/')
} catch (e) {
alert(e)
}
}
return (
<nav className={styles.sidebar}>
<ul>
<li> <Link href="/dashboard"> Dashboard</Link> </li>
<li> <Link href="/dashboard/attendance"> Attendance </Link> </li>
<li> <Link href="/dashboard/attendance-sheet"> Attendance Sheet </Link> </li>
<li onClick={logout}> Logout </li>
</ul>
</nav>
)
}
export default SideBar
现在打开 page/index.js 文件,删除其中的所有代码,并添加以下代码段。下面的代码通过表单向 localhost: 3000/api/login 路由发送包含电子邮件和密码的发送请求。一旦验证了凭据,它就调用 router.push (’/dashboard’)方法将用户重定向到 localhost: 3000/api/dashboard:
JavaScript
import Head from 'next/head'
import { postData } from '../lib/request';
import styles from '../styles/Home.module.css'
import { useState } from 'react';
import { useRouter } from 'next/router'
export default function Home({posts}) {
const [data, setData] = useState({email: null, password: null});
const router = useRouter()
const submit = (e) => {
e.preventDefault()
if(data.email && data.password) {
postData('/api/login', data).then(data => {
console.log(data);
if (data.status === "success") router.push('/dashboard')
});
}
}
return (
<div className={styles.container}>
<Head>
<title>Login</title>
<meta name="description" content="Login" />
<link rel="icon" href="/favicon.ico" />
</Head>
<main className={styles.main}>
<form className={styles.login}>
<input
type={"text"}
placeholder="Enter Your Email"
onChange={(e) => setData({...data, email: e.target.value})} />
<input
type={"password"}
placeholder="Enter Your Password"
onChange={(e) => setData({...data, password: e.target.value})} />
<button onClick={submit}>Login</button>
</form>
</main>
</div>
)
}
现在打开页面/api/login.js 文件并添加以下代码片段。我们将使用 Prismaclient 进行数据库查询,IronSessionApiRoute 是用于处理 RESTful 应用程序中的用户会话的铁会话函数。
此路由处理对 localhost: 3000/api/login 的登录 POST 请求,并在用户通过身份验证后生成身份验证 Cookie。
JavaScript
import { PrismaClient } from '@prisma/client'
import { withIronSessionApiRoute } from "iron-session/next";
import { parseBody } from '../../lib/parseBody';
import { sessionCookie } from '../../lib/session';
export default withIronSessionApiRoute(
async function loginRoute(req, res) {
const { email, password } = parseBody(req.body)
const prisma = new PrismaClient()
// By unique identifier
const user = await prisma.user.findUnique({
where: {
email
},})
if(user.password === password) {
// get user from database then:
user.password = undefined
req.session.user = user
await req.session.save();
return res.send({ status: 'success', data: user });
};
res.send({ status: 'error', message: "incorrect email or password" });
},
sessionCookie(),
);
打开/page/api/logout 文件并添加下面的代码段。此路由处理对 localhost 的 GET 请求: 3000/api/logout,该请求通过销毁会话 cookie 将用户注销。
JavaScript
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionCookie } from "../../lib/session";
export default withIronSessionApiRoute(
function logoutRoute(req, res, session) {
req.session.destroy();
res.send({ status: "success" });
},
sessionCookie()
);
此页面为用户提供了登录和退出考勤表的界面。管理员还可以创建考勤表。打开 page/dashboard/index.js 文件并添加下面的代码片段。
JavaScript
import { withIronSessionSsr } from "iron-session/next";
import Head from 'next/head'
import { useState, useCallback } from "react";
import { PrismaClient } from '@prisma/client'
import SideBar from '../../components/SideBar'
import styles from '../../styles/Home.module.css'
import dashboard from '../../styles/Dashboard.module.css'
import { sessionCookie } from "../../lib/session";
import { postData } from "../../lib/request";
export default function Page(props) {
const [attendanceSheet, setState] = useState(JSON.parse(props.attendanceSheet));
const sign = useCallback((action="") => {
const body = {
attendanceSheetId: attendanceSheet[0]?.id,
action
}
postData("/api/sign-attendance", body).then(data => {
if (data.status === "success") {
setState(prevState => {
const newState = [...prevState]
newState[0].attendance[0] = data.data
return newState
})
}
})
}, [attendanceSheet])
const createAttendance = useCallback(() => {
postData("/api/create-attendance").then(data => {
if (data.status === "success") {
alert("New Attendance Sheet Created")
setState([{...data.data, attendance:[]}])
}
})
}, [])
return (
<div>
<Head>
<title>Attendance Management Dashboard</title>
<meta name="description" content="dashboard" />
</Head>
<div className={styles.navbar}></div>
<main className={styles.dashboard}>
<SideBar />
<div className={dashboard.users}>
{
props.isAdmin && <button className={dashboard.create} onClick={createAttendance}>Create Attendance Sheet</button>
}
{ attendanceSheet.length > 0 &&
<table className={dashboard.table}>
<thead>
<tr>
<th>Id</th> <th>Created At</th> <th>Sign In</th> <th>Sign Out</th>
</tr>
</thead>
<tbody>
<tr>
<td>{attendanceSheet[0]?.id}</td>
<td>{attendanceSheet[0]?.createdAt}</td>
{
attendanceSheet[0]?.attendance.length != 0 ?
<>
<td>{attendanceSheet[0]?.attendance[0]?.signInTime}</td>
<td>{
attendanceSheet[0]?.attendance[0]?.signOut ?
attendanceSheet[0]?.attendance[0]?.signOutTime: <button onClick={() => sign("sign-out")}> Sign Out </button> }</td>
</>
:
<>
<td> <button onClick={() => sign()}> Sign In </button> </td>
<td>{""}</td>
</>
}
</tr>
</tbody>
</table>
}
</div>
</main>
</div>
)
}
我们使用 getServerSideProps 来生成页面数据,而 IronSessionSsr 是用于处理服务器端呈现页面的 Iron-session 函数。在下面的代码片段中,我们使用考勤表中的一行查询考勤表的最后一行,其中 userId 等于存储在用户会话上的 User id。我们还检查用户是否是 ADMIN。
JavaScript
export const getServerSideProps = withIronSessionSsr( async ({req}) => {
const user = req.session.user
const prisma = new PrismaClient()
const attendanceSheet = await prisma.attendanceSheet.findMany({
take: 1,
orderBy: {
id: 'desc',
},
include: {
attendance: {
where: {
userId: user.id
},
}
}
})
return {
props: {
attendanceSheet: JSON.stringify(attendanceSheet),
isAdmin: user.role === "ADMIN"
}
}
}, sessionCookie())
打开页面/api/create-around. js 文件并添加下面的代码片段。
JavaScript
import { PrismaClient } from '@prisma/client'
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionCookie } from '../../lib/session';
export default withIronSessionApiRoute( async function handler(req, res) {
const prisma = new PrismaClient()
const user = req.session.user
const attendanceShe
此路由处理我们对 localhost 的 API POST 请求:
3000/API/sign-publications。该路由接受 POST 请求,而 attanceSheetId 和 action 用于登录和退出 attancesheet。
打开/page/api/sign-around. js 文件并添加下面的代码片段。
JavaScript
import { PrismaClient } from '@prisma/client'
import { withIronSessionApiRoute } from "iron-session/next";
import { parseBody } from '../../lib/parseBody';
import { sessionCookie } from '../../lib/session';
export default withIronSessionApiRoute( async function handler(req, res) {
const prisma = new PrismaClient()
const {attendanceSheetId, action} = parseBody(req.body)
const user = req.session.user
const attendance = await prisma.attendance.findMany({
where: {
userId: user.id,
attendanceSheetId: attendanceSheetId
}
})
//check if atendance have been created
if (attendance.length === 0) {
const attendance = await prisma.attendance.create({
data: {
userId: user.id,
attendanceSheetId: attendanceSheetId,
signIn: true,
signOut: false,
signOutTime: new Date()
},
})
return res.json({status: "success", data: attendance});
} else if (action === "sign-out") {
await prisma.attendance.updateMany({
where: {
userId: user.id,
attendanceSheetId: attendanceSheetId
},
data: {
signOut: true,
signOutTime: new Date()
},
})
return res.json({status: "success", data: { ...attendance[0], signOut: true, signOutTime: new Date()}});
}
res.json({status: "success", data: attendance});
}, sessionCookie())
此服务器端呈现的页面显示了登录用户的所有出勤表。打开
/page/dashboard/attance.js 文件并添加下面的代码片段。
JavaScript
import { withIronSessionSsr } from "iron-session/next";
import Head from 'next/head'
import { PrismaClient } from '@prisma/client'
import SideBar from '../../components/SideBar'
import styles from '../../styles/Home.module.css'
import dashboard from '../../styles/Dashboard.module.css'
import { sessionCookie } from "../../lib/session";
export default function Page(props) {
const data = JSON.parse(props.attendanceSheet)
return (
<div>
<Head>
<title>Attendance Management Dashboard</title>
<meta name="description" content="dashboard" />
</Head>
<div className={styles.navbar}></div>
<main className={styles.dashboard}>
<SideBar />
<div className={dashboard.users}>
<table className={dashboard.table}>
<thead>
<tr>
<th> Attendance Id</th> <th>Date</th>
<th>Sign In Time</th> <th>Sign Out Time</th>
</tr>
</thead>
<tbody>
{
data.map(data => {
const {id, createdAt, attendance } = data
return (
<tr key={id}>
<td>{id}</td> <td>{createdAt}</td>
{ attendance.length === 0 ?
(
<>
<td>You did not Sign In</td>
<td>You did not Sign Out</td>
</>
)
:
(
<>
<td>{attendance[0]?.signInTime}</td>
<td>{attendance[0]?.signOut ? attendance[0]?.signOutTime : "You did not Sign Out"}</td>
</>
)
}
</tr>
)
})
}
</tbody>
</table>
</div>
</main>
</div>
)
}
In the code snippet below, we query for all the rows from the attendanceSheet table and also fetch the attendance where the userId is equal to the user id stored in the user session.
export const getServerSideProps = withIronSessionSsr( async ({req}) => {
const user = req.session.user
const prisma = new PrismaClient()
const attendanceSheet = await prisma.attendanceSheet.findMany({
orderBy: {
id: 'desc',
},
include: {
attendance: {
where: {
userId: user.id
},
}
}
})
return {
props: {
attendanceSheet: JSON.stringify(attendanceSheet),
}
}
}, sessionCookie())
这个服务器端呈现的页面显示了所有的考勤表以及在该考勤表上签名的员工。打开
/page/dashboard/attance.js 文件并添加下面的代码片段。
JavaScript
import { withIronSessionSsr } from "iron-session/next";
import Head from 'next/head'
import { PrismaClient } from '@prisma/client'
import SideBar from '../../components/SideBar'
import styles from '../../styles/Home.module.css'
import dashboard from '../../styles/Dashboard.module.css'
import { sessionCookie } from "../../lib/session";
export default function Page(props) {
const data = JSON.parse(props.attendanceSheet)
return (
<div>
<Head>
<title>Attendance Management Dashboard</title>
<meta name="description" content="dashboard" />
</Head>
<div className={styles.navbar}></div>
<main className={styles.dashboard}>
<SideBar />
<div className={dashboard.users}>
{
data?.map(data => {
const {id, createdAt, attendance } = data
return (
<>
<table key={data.id} className={dashboard.table}>
<thead>
<tr>
<th> Attendance Id</th> <th>Date</th>
<th> Name </th> <th> Email </th> <th> Role </th>
<th>Sign In Time</th> <th>Sign Out Time</th>
</tr>
</thead>
<tbody>
{
(attendance.length === 0) &&
(
<>
<tr><td> {id} </td> <td>{createdAt}</td> <td colSpan={5}> No User signed this sheet</td></tr>
</>
)
}
{
attendance.map(data => {
const {name, email, role} = data.user
return (
<tr key={id}>
<td>{id}</td> <td>{createdAt}</td>
<td>{name}</td> <td>{email}</td>
<td>{role}</td>
<td>{data.signInTime}</td>
<td>{data.signOut ? attendance[0]?.signOutTime: "User did not Sign Out"}</td>
</tr>
)
})
}
</tbody>
</table>
</>
)
})
}
</div>
</main>
</div>
)
}
在下面的代码片段中,我们将查询 attancesheet 表中的所有行,并通过选择姓名、电子邮件和角色来获取出席率。
JavaScript
export const getServerSideProps = withIronSessionSsr(async () => {
const prisma = new PrismaClient()
const attendanceSheet = await prisma.attendanceSheet.findMany({
orderBy: {
id: 'desc',
},
include: {
attendance: {
include: {
user: {
select: {
name: true,
email: true,
role: true
}
}
}
},
},
})
return {
props: {
attendanceSheet: JSON.stringify(attendanceSheet),
}
}
}, sessionCookie())
首先,我们必须将用户添加到数据库中。我们要用Prisma做这个。要启动 Prisma ,请运行以下命令:
Shell
npx prisma studio
Prisma 索引页面如下:
要创建一个具有 ADMIN 角色的数据库用户和多个具有 EMPLOYEE 角色的用户,请访问以下页面:
单击 Add record,然后填写所需的字段: password、 name、 email 和 role。完成后,单击绿色的 Save 1 change 按钮。注意,为了简单起见,我们没有散列密码。
用yarn dev启动服务器。这将启动服务器并在[ localhost: 3000](< http://localhost:3000)上运行应用程序,登录页面如下所示。
使用具有 ADMIN 角色的用户登录,因为只有管理用户可以创建考勤表。一旦登录成功,应用程序将重定向到您的仪表板。
单击 CreateAtnatureShet 按钮创建考勤表,然后等待请求完成,考勤表将出现。用户仪表板如下所示。
出勤表如下所示,单击“登录”按钮即可登录。登录成功后,将显示登录时间,并且可以看到 Sign Out 按钮。单击“登出”按钮登出,并与不同的用户多次重复此过程。
接下来点击侧边栏中的出席链接,查看用户的出席情况。结果应与下列结果相符:
接下来点击侧边栏上的出勤表链接,查看所有用户的出勤情况。结果如下:
在本文中,您学习了如何在 Next.js 中使用自定义 Fastify 服务器。您还了解了 Prisma 和 Prisma 工作室。我已经向您介绍了如何将 Prisma 连接到 Postgres 数据库,以及如何使用 Prisma 客户端和 Prisma studio创建、读取和更新数据库。
您还学习了如何使用铁会话对用户进行身份验证。在本教程中,我们构建了一个完整的应用程序,它使用 Next.js、 Prisma、 Postgres 和 Fastify 管理员工出勤率。请继续收听,下次再见。