前言
因为是练手项目,所以我们用户校验使用jwt
,而不是Clerk
等,欢迎来到这一节
token配置
安装依赖:
pnpm i jsonwebtoken
pnpm i @types/jsonwebtoken -D
新建helpers/auth.ts
用于生成和解析token
import jwt from 'jsonwebtoken';
import type { NextRequest } from 'next/server';
const TOKEN_SECRET = process.env.NEXT_PUBLIC_ACCESS_TOKEN_SECRET || '';
export async function verifyToken(req: NextRequest, isJwt: boolean) {
try {
const token = req?.headers?.get('authorization');
if (!token) {
throw new Error('No token provided');
}
const decoded: any = jwt.verify(token, TOKEN_SECRET);
const id = decoded?.id;
return new Promise((resolve) => resolve(id));
} catch (error) {
if (isJwt) {
throw error;
}
}
}
export async function createAccessToken(payload: any) {
if (!payload) {
throw new Error('No payload provided');
}
return jwt.sign(payload, TOKEN_SECRET, {
expiresIn: '1d',
});
}
NEXT_PUBLIC_ACCESS_TOKEN_SECRET
自行在.env
下面自定义一个字符串即可
然后我们新建helpers/jwt-middleware.ts
用于校验解析authorization
并将解析后的userId
保存到headers
里面
import type { NextRequest } from 'next/server';
import { verifyToken } from './auth';
export default async function jwtMiddleware(req: NextRequest, isJwt: boolean = false) {
const id: string = (await verifyToken(req, isJwt)) as string;
req.headers.set('userId', id);
}
修改helpers/apiHandler.ts
,使用我们新增的jwt
中间件
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { errorHandler } from './errorHandler';
import validateMiddleware from './validate-middleware';
import jwtMiddleware from './jwt-middleware';
export function apiHandler(
handler: (req: NextRequest, ...args: any[]) => any,
{ isJwt, schema }: { isJwt?: boolean; schema?: any } = {},
) {
return async (req: NextRequest, ...args: any[]) => {
try {
await jwtMiddleware(req, isJwt);
await validateMiddleware(req, schema);
const responseBody = await handler(req, ...args);
return NextResponse.json(responseBody || {});
} catch (err: any) {
return errorHandler(err);
}
};
}
prisma配置
修改prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
directUrl = env("DIRECT_URL")
}
// 用户
model User {
id String @id @default(cuid())
email String @unique
name String
password String
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
createdSystems System[] @relation("CreatedSystems")
updatedSystems System[] @relation("UpdatedSystems")
createdRoles Role[] @relation("CreatedRoles")
updatedRoles Role[] @relation("UpdatedRoles")
userRoles UserRole[]
@@index([email])
}
// 系统
model System {
id String @id @default(cuid())
name String
description String?
status STATUS? @default(ENABLED)
creator User @relation("CreatedSystems", fields: [creatorId], references: [id])
creatorId String
updator User? @relation("UpdatedSystems", fields: [updatorId], references: [id])
updatorId String?
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
resources Resource[]
privileges Privilege[]
roles Role[]
}
// 资源
model Resource {
id String @id @default(cuid())
name String
// 对应资源的可识别key,并不等同于系统自建id
key String @unique
// 菜单类的资源才会有排序的功能
sort Int? @default(0)
// 父子嵌套,当为null为顶级资源
parent Resource? @relation("ParentChild", fields: [parentId], references: [id])
parentId String?
children Resource[] @relation("ParentChild")
system System? @relation(fields: [systemId], references: [id])
systemId String?
// 资源类型
type RESOURCE_TYPE @default(NORMAL)
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
privileges Privilege[]
@@index([key])
}
// 权限管理
model Privilege {
id String @id @default(cuid())
system System @relation(fields: [systemId], references: [id])
systemId String
resource Resource @relation(fields: [resourceKey], references: [key])
resourceKey String
name String
description String?
action ACTION
status PRIVILEGE_STATUS @default(ALLOW)
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
rolePrivileges RolePrivilege[]
}
// 角色管理
model Role {
id String @id @default(cuid())
name String
description String?
creator User @relation("CreatedRoles", fields: [createdId], references: [id])
createdId String
updator User @relation("UpdatedRoles", fields: [updatedId], references: [id])
updatedId String
system System @relation(fields: [systemId], references: [id])
systemId String
status STATUS @default(ENABLED)
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
rolePrivileges RolePrivilege[]
userRoles UserRole[]
@@index([name])
}
// 角色 <-> 权限
model RolePrivilege {
role Role @relation(fields: [roleId], references: [id])
roleId String
privilege Privilege @relation(fields: [privilegeId], references: [id])
privilegeId String
createdAt DateTime @default(now())
@@id([roleId, privilegeId])
}
// 用户<->角色
model UserRole {
user User @relation(fields: [userId], references: [id])
userId String
role Role @relation(fields: [roleId], references: [id])
roleId String
createdAt DateTime @default(now())
@@id([userId, roleId])
}
enum STATUS {
DISABLED
ENABLED
}
enum RESOURCE_TYPE {
MENU
NORMAL
}
enum ACTION {
MANAGE
CREATE
READ
UPDATE
DELETE
}
enum PRIVILEGE_STATUS {
DENY
ALLOW
NOT_SET
}
其中包含以下模块:
- 用户模块
- 系统模块
- 资源模块
- 权限模块
- 角色模块
- 角色权限多对多
- 用户角色多对多
为啥会将角色权限和用户角色拆成单独的表呢,因为可以可以更加灵活管理,且包含createdAt可以更好追踪数据的变更,本用户服务基于RBAC模型设计
这里就不过多阐述了有兴趣可以直接查看小册的设计篇juejin.cn/book/691897…
现在我们更新了prisma
配置,需要依次运行如下命令:
npx prisma migrate reset
npx prisma migrate dev
运行npx prisma studio
可以查看新的配置是否应用到数据库
登录注册
先定义登录注册的数据校验吧
新建schemas/auth.ts
import { z } from 'zod';
export const signUpSchema = z.object({
email: z.string().email(),
name: z.string().min(2),
password: z.string().min(6),
});
export type signUpSchema = z.infer<typeof signUpSchema>;
export const signInSchema = z.object({
email: z.string().email(),
password: z.string().min(6),
});
export type signInSchema = z.infer<typeof signInSchema>;
主要就是利用zod
定义schema
然后我们就可以开始数据库相关的逻辑了
新建helpers/dbRepo/auth.ts
import bcrypt from 'bcryptjs';
import { createAccessToken } from '@/helpers/auth';
import prisma from '@/helpers/prisma';
import type { signUpSchema, signInSchema } from '@/schemas/auth';
async function signUp(data: signUpSchema) {
const { email, name, password } = data;
if (
await prisma.user.findUnique({
where: {
email,
},
})
) {
const userExistsError = new Error('email ' + email + '账户已存在');
userExistsError.name = 'UserExistsError';
throw userExistsError;
}
const hashPassword = await bcrypt.hash(password, 12);
const newUser = { name, email, password: hashPassword };
const result = await prisma.user.create({
data: newUser,
});
const token = await createAccessToken({ id: result.id });
return {
user: {
name: result.name,
email: result.email,
},
token,
};
}
async function signIn(data: signInSchema) {
const { email, password } = data;
const user = await prisma.user.findUnique({
where: {
email,
},
});
if (!user) {
throw new Error('用户不存在');
}
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
throw new Error('邮箱或密码错误');
}
const token = await createAccessToken({ id: user.id });
return {
user: {
name: user.name,
email: user.email,
},
token,
};
}
const authRepo = {
signUp,
signIn,
};
export default authRepo;
这里需要安装一下bcrypt
依赖
pnpm i bcryptjs
pnpm i @types/bcryptjs -D
分别是:
- 注册:注册的时候查看是否是已有用户,将
password
进行加密,然后注册用户,并且生成token
返回 - 登录:根据
email
查询用户,匹配密码,最后返回数据
接口
新建app/api/signup/route.ts
import authRepo from '@/helpers/dbRepo/auth';
import { apiHandler, transformInterceptor } from '@/helpers';
import type { NextRequest } from 'next/server';
import { signUpSchema } from '@/schemas/auth';
const signUp = apiHandler(
async (req: NextRequest) => {
const body = await req.json();
const result = await authRepo.signUp(body);
return transformInterceptor({
data: result,
});
},
{
schema: signUpSchema,
},
);
export const POST = signUp;
新建app/api/signin/route.ts
import { apiHandler, transformInterceptor } from '@/helpers';
import authRepo from '@/helpers/dbRepo/auth';
import { signInSchema } from '@/schemas/auth';
import type { NextRequest } from 'next/server';
const signIn = apiHandler(
async (req: NextRequest) => {
const body = await req.json();
const result = await authRepo.signIn(body);
return transformInterceptor({
data: result,
});
},
{
schema: signInSchema,
},
);
export const POST = signIn;
简单的逻辑,想必不用过多解释了