From b8c73030d8cee090f2dbf72be7b8fd42e7b5b956 Mon Sep 17 00:00:00 2001 From: longdayi <13477510+longdayilongdayi@user.noreply.gitee.com> Date: Mon, 9 Sep 2024 18:48:07 +0800 Subject: [PATCH] 09091848 --- .gitignore | 5 +- apps/server/package.json | 1 + apps/server/src/app.module.ts | 3 +- apps/server/src/auth/auth.controller.ts | 41 ++++ apps/server/src/auth/auth.guard.ts | 3 +- apps/server/src/auth/auth.module.ts | 9 +- apps/server/src/auth/auth.router.ts | 25 -- apps/server/src/auth/auth.service.ts | 221 ++++++++++-------- apps/server/src/models/staff/staff.module.ts | 11 +- apps/server/src/models/staff/staff.router.ts | 9 +- apps/server/src/rbac/rbac.module.ts | 14 ++ apps/server/src/rbac/role.router.ts | 37 +++ apps/server/src/rbac/role.service.ts | 134 +++++++++++ apps/server/src/rbac/rolemap.router.ts | 59 +++++ apps/server/src/rbac/rolemap.service.ts | 215 +++++++++++++++++ apps/server/src/transform/transform.module.ts | 9 +- apps/server/src/trpc/trpc.module.ts | 12 +- apps/server/src/trpc/trpc.router.ts | 3 - apps/server/src/trpc/trpc.service.ts | 8 +- apps/web/index.html | 31 ++- apps/web/package.json | 9 +- apps/web/src/App.tsx | 9 +- apps/web/src/app/login.tsx | 3 + apps/web/src/app/main/page.tsx | 1 - apps/web/src/components/auth/with-auth.tsx | 29 +++ apps/web/src/env.ts | 13 ++ apps/web/src/providers/auth-provider.tsx | 159 +++++++++++++ apps/web/src/providers/query-provider.tsx | 22 +- apps/web/src/routes/index.tsx | 11 +- apps/web/src/utils/axios-client.ts | 20 ++ packages/common/prisma/schema.prisma | 23 +- packages/common/src/enum.ts | 2 + packages/common/src/schema.ts | 73 +++++- packages/common/src/type.ts | 26 ++- 34 files changed, 1056 insertions(+), 194 deletions(-) create mode 100644 apps/server/src/auth/auth.controller.ts delete mode 100644 apps/server/src/auth/auth.router.ts create mode 100644 apps/server/src/rbac/rbac.module.ts create mode 100644 apps/server/src/rbac/role.router.ts create mode 100644 apps/server/src/rbac/role.service.ts create mode 100644 apps/server/src/rbac/rolemap.router.ts create mode 100644 apps/server/src/rbac/rolemap.service.ts create mode 100644 apps/web/src/app/login.tsx create mode 100644 apps/web/src/components/auth/with-auth.tsx create mode 100644 apps/web/src/env.ts create mode 100644 apps/web/src/providers/auth-provider.tsx create mode 100644 apps/web/src/utils/axios-client.ts diff --git a/.gitignore b/.gitignore index 39d3cfd..81de94c 100644 --- a/.gitignore +++ b/.gitignore @@ -229,4 +229,7 @@ $RECYCLE.BIN/ # Linux *~ -docker-compose.yml \ No newline at end of file +docker-compose.yml +.env +packages/common/prisma/migrations +volumes \ No newline at end of file diff --git a/apps/server/package.json b/apps/server/package.json index 7aae1df..738b7e0 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -26,6 +26,7 @@ "@nestjs/core": "^10.0.0", "@nestjs/jwt": "^10.2.0", "@nestjs/platform-express": "^10.0.0", + "@nestjs/platform-socket.io": "^10.4.1", "@nestjs/schedule": "^4.1.0", "@nestjs/websockets": "^10.3.10", "@nicestack/common": "workspace:^", diff --git a/apps/server/src/app.module.ts b/apps/server/src/app.module.ts index 1559af9..5814881 100644 --- a/apps/server/src/app.module.ts +++ b/apps/server/src/app.module.ts @@ -8,9 +8,10 @@ import { QueueModule } from './queue/queue.module'; import { TransformModule } from './transform/transform.module'; import { AuthModule } from './auth/auth.module'; import { ScheduleModule } from '@nestjs/schedule'; +import { ConfigService } from '@nestjs/config'; @Module({ imports: [ScheduleModule.forRoot(), TrpcModule, RedisModule, QueueModule, TransformModule, AuthModule], - providers: [RedisService, SocketGateway], + providers: [RedisService, SocketGateway, ConfigService], }) export class AppModule { } diff --git a/apps/server/src/auth/auth.controller.ts b/apps/server/src/auth/auth.controller.ts new file mode 100644 index 0000000..4ec6a74 --- /dev/null +++ b/apps/server/src/auth/auth.controller.ts @@ -0,0 +1,41 @@ +import { Controller, Post, Body, UseGuards, Get, Req } from '@nestjs/common'; +import { AuthService } from './auth.service'; +import { AuthSchema, JwtPayload } from '@nicestack/common'; + +import { z } from 'zod'; +import { AuthGuard } from './auth.guard'; + +@Controller('auth') +export class AuthController { + constructor(private readonly authService: AuthService) { } + @Get("user-profile") + async getUserProfile(@Req() request: Request) { + const user: JwtPayload = (request as any).user + return this.authService.getUserProfile(user) + } + @Post('login') + async login(@Body() body: z.infer) { + return this.authService.signIn(body); + } + + @Post('signup') + async signup(@Body() body: z.infer) { + return this.authService.signUp(body); + } + + @UseGuards(AuthGuard) // Protecting the refreshToken endpoint with AuthGuard + @Post('refresh-token') + async refreshToken(@Body() body: z.infer) { + return this.authService.refreshToken(body); + } + @UseGuards(AuthGuard) // Protecting the logout endpoint with AuthGuard + @Post('logout') + async logout(@Body() body: z.infer) { + return this.authService.logout(body); + } + @UseGuards(AuthGuard) // Protecting the changePassword endpoint with AuthGuard + @Post('change-password') + async changePassword(@Body() body: z.infer) { + return this.authService.changePassword(body); + } +} diff --git a/apps/server/src/auth/auth.guard.ts b/apps/server/src/auth/auth.guard.ts index a2bd1d3..22c2383 100644 --- a/apps/server/src/auth/auth.guard.ts +++ b/apps/server/src/auth/auth.guard.ts @@ -7,6 +7,7 @@ import { import { JwtService } from '@nestjs/jwt'; import { env } from '@server/env'; import { Request } from 'express'; +import { JwtPayload } from '@nicestack/common'; @Injectable() export class AuthGuard implements CanActivate { @@ -18,7 +19,7 @@ export class AuthGuard implements CanActivate { throw new UnauthorizedException(); } try { - const payload = await this.jwtService.verifyAsync( + const payload: JwtPayload = await this.jwtService.verifyAsync( token, { secret: env.JWT_SECRET diff --git a/apps/server/src/auth/auth.module.ts b/apps/server/src/auth/auth.module.ts index d60f0a9..e4593f0 100644 --- a/apps/server/src/auth/auth.module.ts +++ b/apps/server/src/auth/auth.module.ts @@ -2,13 +2,18 @@ import { Module } from '@nestjs/common'; import { AuthService } from './auth.service'; import { JwtModule } from '@nestjs/jwt'; import { env } from '@server/env'; +import { AuthController } from './auth.controller'; +import { StaffService } from '@server/models/staff/staff.service'; +import { RoleMapService } from '@server/rbac/rolemap.service'; +import { DepartmentService } from '@server/models/department/department.service'; @Module({ - providers: [AuthService], + providers: [AuthService, StaffService, RoleMapService,DepartmentService], imports: [JwtModule.register({ global: true, secret: env.JWT_SECRET, signOptions: { expiresIn: '60s' }, - }),] + }),], + controllers: [AuthController] }) export class AuthModule { } diff --git a/apps/server/src/auth/auth.router.ts b/apps/server/src/auth/auth.router.ts deleted file mode 100644 index 5be4d81..0000000 --- a/apps/server/src/auth/auth.router.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { TrpcService } from '@server/trpc/trpc.service'; -import { AuthService } from './auth.service'; -import { AuthSchema } from '@nicestack/common'; -@Injectable() -export class AuthRouter { - constructor(private readonly trpc: TrpcService, private readonly authService: AuthService) { } - router = this.trpc.router({ - login: this.trpc.procedure.input(AuthSchema.signInRequset).mutation(({ input }) => { - return this.authService.signIn(input); - }), - signup: this.trpc.procedure.input(AuthSchema.signUpRequest).mutation(({ input }) => { - return this.authService.signUp(input); - }), - refreshToken: this.trpc.procedure.input(AuthSchema.refreshTokenRequest).mutation(({ input }) => { - return this.authService.refreshToken(input); - }), - logout: this.trpc.protectProcedure.input(AuthSchema.logoutRequest).mutation(({ input }) => { - return this.authService.logout(input); - }), - changePassword: this.trpc.protectProcedure.input(AuthSchema.changePassword).mutation(({ input }) => { - return this.authService.changePassword(input); - }), - }); -} diff --git a/apps/server/src/auth/auth.service.ts b/apps/server/src/auth/auth.service.ts index c5d9092..7b6d3f7 100644 --- a/apps/server/src/auth/auth.service.ts +++ b/apps/server/src/auth/auth.service.ts @@ -1,120 +1,157 @@ import { - Injectable, - UnauthorizedException, - BadRequestException, - NotFoundException, + Injectable, + UnauthorizedException, + BadRequestException, + NotFoundException, } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import * as bcrypt from 'bcrypt'; import { AuthSchema, db, z } from '@nicestack/common'; import { StaffService } from '@server/models/staff/staff.service'; +import { JwtPayload } from '@nicestack/common'; +import { RoleMapService } from '@server/rbac/rolemap.service'; @Injectable() export class AuthService { - constructor( - private readonly jwtService: JwtService, - private readonly staffService: StaffService, - ) { } - async signIn(data: z.infer) { - const { username, password } = data; - const staff = await db.staff.findUnique({ where: { username } }); - if (!staff) { - throw new UnauthorizedException('Invalid username or password'); - } - const isPasswordMatch = await bcrypt.compare(password, staff.password); + constructor( + private readonly jwtService: JwtService, + private readonly staffService: StaffService, + private readonly roleMapService: RoleMapService + ) { } - if (!isPasswordMatch) { - throw new UnauthorizedException('Invalid username or password'); - } - - const payload = { sub: staff.id, username: staff.username }; - const accessToken = await this.jwtService.signAsync(payload, { expiresIn: '1h' }); - const refreshToken = await this.generateRefreshToken(staff.id); - - // Store the refresh token in the database - await db.refreshToken.create({ - data: { - staffId: staff.id, - token: refreshToken, - }, - }); - return { - access_token: accessToken, - refresh_token: refreshToken, - }; + async signIn(data: z.infer) { + const { username, password } = data; + const staff = await db.staff.findUnique({ where: { username } }); + if (!staff) { + throw new UnauthorizedException('Invalid username or password'); } - async generateRefreshToken(userId: string): Promise { - const payload = { sub: userId }; - return this.jwtService.signAsync(payload, { expiresIn: '7d' }); // Set an appropriate expiration + const isPasswordMatch = await bcrypt.compare(password, staff.password); + if (!isPasswordMatch) { + throw new UnauthorizedException('Invalid username or password'); } - async refreshToken(data: z.infer) { - const { refreshToken } = data; - let payload; - try { - payload = this.jwtService.verify(refreshToken); - } catch (error) { - throw new UnauthorizedException('Invalid refresh token'); - } + const payload: JwtPayload = { sub: staff.id, username: staff.username }; + const accessToken = await this.jwtService.signAsync(payload, { expiresIn: '1h' }); + const refreshToken = await this.generateRefreshToken(staff.id); - const storedToken = await db.refreshToken.findUnique({ where: { token: refreshToken } }); + // Calculate expiration dates + const accessTokenExpiresAt = new Date(); + accessTokenExpiresAt.setHours(accessTokenExpiresAt.getHours() + 1); - if (!storedToken) { - throw new UnauthorizedException('Invalid refresh token'); - } + const refreshTokenExpiresAt = new Date(); + refreshTokenExpiresAt.setDate(refreshTokenExpiresAt.getDate() + 7); - const user = await db.staff.findUnique({ where: { id: payload.sub } }); - if (!user) { - throw new UnauthorizedException('Invalid refresh token'); - } + // Store the refresh token in the database + await db.refreshToken.create({ + data: { + staffId: staff.id, + token: refreshToken, + }, + }); - const newAccessToken = await this.jwtService.signAsync( - { sub: user.id, username: user.username }, - { expiresIn: '1h' }, - ); + return { + access_token: accessToken, + access_token_expires_at: accessTokenExpiresAt, + refresh_token: refreshToken, + refresh_token_expires_at: refreshTokenExpiresAt, + }; + } - return { - access_token: newAccessToken, - }; + async generateRefreshToken(userId: string): Promise { + const payload = { sub: userId }; + return this.jwtService.signAsync(payload, { expiresIn: '7d' }); // Set an appropriate expiration + } + + async refreshToken(data: z.infer) { + const { refreshToken } = data; + + let payload: JwtPayload; + try { + payload = this.jwtService.verify(refreshToken); + } catch (error) { + throw new UnauthorizedException('Invalid refresh token'); } - async signUp(data: z.infer) { - const { username, password } = data; - const existingUser = await db.staff.findUnique({ where: { username } }); - - if (existingUser) { - throw new BadRequestException('Username is already taken'); - } - - const hashedPassword = await bcrypt.hash(password, 10); - const staff = await this.staffService.create({ - username, - password: hashedPassword, - }); - - return staff + const storedToken = await db.refreshToken.findUnique({ where: { token: refreshToken } }); + if (!storedToken) { + throw new UnauthorizedException('Invalid refresh token'); } - async logout(data: z.infer) { - const { refreshToken } = data; - await db.refreshToken.deleteMany({ where: { token: refreshToken } }); - return { message: 'Logout successful' }; + const user = await db.staff.findUnique({ where: { id: payload.sub } }); + if (!user) { + throw new UnauthorizedException('Invalid refresh token'); } - async changePassword(data: z.infer) { - const { oldPassword, newPassword, username } = data; - const user = await db.staff.findUnique({ where: { username } }); - if (!user) { - throw new NotFoundException('User not found'); - } - const isPasswordMatch = await bcrypt.compare(oldPassword, user.password); - if (!isPasswordMatch) { - throw new UnauthorizedException('Old password is incorrect'); - } - const hashedNewPassword = await bcrypt.hash(newPassword, 10); - await this.staffService.update({ id: user.id, password: hashedNewPassword }); + const newAccessToken = await this.jwtService.signAsync( + { sub: user.id, username: user.username }, + { expiresIn: '1h' }, + ); - return { message: 'Password successfully changed' }; + // Calculate new expiration date + const accessTokenExpiresAt = new Date(); + accessTokenExpiresAt.setHours(accessTokenExpiresAt.getHours() + 1); + + return { + access_token: newAccessToken, + access_token_expires_at: accessTokenExpiresAt, + }; + } + + async signUp(data: z.infer) { + const { username, password } = data; + const existingUser = await db.staff.findUnique({ where: { username } }); + + if (existingUser) { + throw new BadRequestException('Username is already taken'); } + + const hashedPassword = await bcrypt.hash(password, 10); + const staff = await this.staffService.create({ + username, + password: hashedPassword, + }); + + return staff; + } + + async logout(data: z.infer) { + const { refreshToken } = data; + await db.refreshToken.deleteMany({ where: { token: refreshToken } }); + return { message: 'Logout successful' }; + } + + async changePassword(data: z.infer) { + const { oldPassword, newPassword, username } = data; + const user = await db.staff.findUnique({ where: { username } }); + + if (!user) { + throw new NotFoundException('User not found'); + } + + const isPasswordMatch = await bcrypt.compare(oldPassword, user.password); + if (!isPasswordMatch) { + throw new UnauthorizedException('Old password is incorrect'); + } + + const hashedNewPassword = await bcrypt.hash(newPassword, 10); + await this.staffService.update({ id: user.id, password: hashedNewPassword }); + + return { message: 'Password successfully changed' }; + } + async getUserProfile(data: JwtPayload) { + const { sub } = data + const staff = await db.staff.findUnique({ + where: { id: sub }, include: { + department: true, + domain: true + } + }) + const staffPerms = await this.roleMapService.getPermsForObject({ + domainId: staff.domainId, + staffId: staff.id, + deptId: staff.deptId, + }); + return { ...staff, permissions: staffPerms } + } } diff --git a/apps/server/src/models/staff/staff.module.ts b/apps/server/src/models/staff/staff.module.ts index 871eced..c7240ed 100644 --- a/apps/server/src/models/staff/staff.module.ts +++ b/apps/server/src/models/staff/staff.module.ts @@ -1,4 +1,11 @@ import { Module } from '@nestjs/common'; +import { StaffRouter } from './staff.router'; +import { StaffService } from './staff.service'; +import { TrpcService } from '@server/trpc/trpc.service'; +import { DepartmentService } from '../department/department.service'; -@Module({}) -export class StaffModule {} +@Module({ + providers: [StaffRouter, StaffService, TrpcService, DepartmentService], + exports: [StaffRouter, StaffService] +}) +export class StaffModule { } diff --git a/apps/server/src/models/staff/staff.router.ts b/apps/server/src/models/staff/staff.router.ts index a0196aa..f08dde0 100755 --- a/apps/server/src/models/staff/staff.router.ts +++ b/apps/server/src/models/staff/staff.router.ts @@ -8,7 +8,7 @@ export class StaffRouter { constructor( private readonly trpc: TrpcService, private readonly staffService: StaffService, - ) {} + ) { } router = this.trpc.router({ create: this.trpc.procedure @@ -43,11 +43,6 @@ export class StaffRouter { .input(StaffSchema.findMany) // Assuming StaffSchema.findMany is the Zod schema for finding staffs by keyword .query(async ({ input }) => { return await this.staffService.findMany(input); - }), - findUnique: this.trpc.procedure - .input(StaffSchema.findUnique) - .query(async ({ input }) => { - return await this.staffService.findUnique(input); - }), + }) }); } diff --git a/apps/server/src/rbac/rbac.module.ts b/apps/server/src/rbac/rbac.module.ts new file mode 100644 index 0000000..210b5b3 --- /dev/null +++ b/apps/server/src/rbac/rbac.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { RoleMapService } from './rolemap.service'; +import { RoleRouter } from './role.router'; +import { TrpcService } from '@server/trpc/trpc.service'; +import { RoleService } from './role.service'; +import { RoleMapRouter } from './rolemap.router'; +import { DepartmentModule } from '@server/models/department/department.module'; + +@Module({ + imports: [DepartmentModule], + providers: [RoleMapService, RoleRouter, TrpcService, RoleService, RoleMapRouter], + exports: [RoleRouter, RoleService, RoleMapService, RoleMapRouter] +}) +export class RoleMapModule { } diff --git a/apps/server/src/rbac/role.router.ts b/apps/server/src/rbac/role.router.ts new file mode 100644 index 0000000..274229f --- /dev/null +++ b/apps/server/src/rbac/role.router.ts @@ -0,0 +1,37 @@ +import { Injectable } from "@nestjs/common"; +import { TrpcService } from "@server/trpc/trpc.service"; +import { RoleService } from "./role.service"; +import { z, RoleSchema } from "@nicestack/common"; + +@Injectable() +export class RoleRouter { + constructor( + private readonly trpc: TrpcService, + private readonly roleService: RoleService + ) { } + + router = this.trpc.router({ + create: this.trpc.protectProcedure.input(RoleSchema.create).mutation(async ({ ctx, input }) => { + const { staff } = ctx; + return await this.roleService.create(input); + }), + batchDelete: this.trpc.protectProcedure.input(RoleSchema.batchDelete).mutation(async ({ input }) => { + return await this.roleService.batchDelete(input); + }), + update: this.trpc.protectProcedure.input(RoleSchema.update).mutation(async ({ ctx, input }) => { + const { staff } = ctx; + return await this.roleService.update(input); + }), + paginate: this.trpc.protectProcedure.input(RoleSchema.paginate).query(async ({ ctx, input }) => { + const { staff } = ctx; + return await this.roleService.paginate(input); + }), + findMany: this.trpc.procedure + .input(RoleSchema.findMany) // Assuming StaffSchema.findMany is the Zod schema for finding staffs by keyword + .query(async ({ input }) => { + return await this.roleService.findMany(input); + }) + } + ) +} + diff --git a/apps/server/src/rbac/role.service.ts b/apps/server/src/rbac/role.service.ts new file mode 100644 index 0000000..685f739 --- /dev/null +++ b/apps/server/src/rbac/role.service.ts @@ -0,0 +1,134 @@ +import { Injectable } from '@nestjs/common'; +import { db, z, RoleSchema, ObjectType, Role, RoleMap } from "@nicestack/common"; +import { DepartmentService } from '@server/models/department/department.service'; +import { TRPCError } from '@trpc/server'; + +@Injectable() +export class RoleService { + constructor( + private readonly departmentService: DepartmentService + ) { } + + /** + * 创建角色 + * @param data 包含创建角色所需信息的数据 + * @returns 创建的角色 + */ + async create(data: z.infer) { + + // 开启事务 + return await db.$transaction(async (prisma) => { + // 创建角色 + return await prisma.role.create({ data }); + }); + } + + /** + * 更新角色 + * @param data 包含更新角色所需信息的数据 + * @returns 更新后的角色 + */ + async update(data: z.infer) { + const { id, ...others } = data; + + // 开启事务 + return await db.$transaction(async (prisma) => { + // 更新角色 + const updatedRole = await prisma.role.update({ + where: { id }, + data: { ...others } + }); + + return updatedRole; + }); + } + + /** + * 批量删除角色 + * @param data 包含要删除的角色ID列表的数据 + * @returns 删除结果 + * @throws 如果未提供ID,将抛出错误 + */ + async batchDelete(data: z.infer) { + const { ids } = data; + if (!ids || ids.length === 0) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'No IDs provided for deletion.' + }); + } + + // 开启事务 + return await db.$transaction(async (prisma) => { + const deletedRoles = await prisma.role.updateMany({ + where: { + id: { in: ids } + }, + data: { deletedAt: new Date() } + }); + + await prisma.roleMap.deleteMany({ + where: { + roleId: { + in: ids + } + } + }); + + if (!deletedRoles.count) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: 'No roles were found with the provided IDs.' + }); + } + + return { success: true, count: deletedRoles.count }; + }); + } + + /** + * 分页获取角色 + * @param data 包含分页信息的数据 + * @returns 分页结果,包含角色列表和总数 + */ + async paginate(data: z.infer) { + const { page, pageSize } = data; + const [items, totalCount] = await Promise.all([ + db.role.findMany({ + skip: (page - 1) * pageSize, + take: pageSize, + orderBy: { name: "asc" }, + where: { deletedAt: null }, + include: { + roleMaps: true, + } + }), + db.role.count({ where: { deletedAt: null } }), + ]); + const result = { items, totalCount }; + return result; + } + + /** + * 根据关键字查找多个角色 + * @param data 包含关键字的数据 + * @returns 查找到的角色列表 + */ + async findMany(data: z.infer) { + const { keyword } = data + return await db.role.findMany({ + where: { + deletedAt: null, + OR: [ + { + name: { + contains: keyword + } + } + ] + }, + orderBy: { name: "asc" }, + take: 10 + }) + } +} diff --git a/apps/server/src/rbac/rolemap.router.ts b/apps/server/src/rbac/rolemap.router.ts new file mode 100644 index 0000000..1362e28 --- /dev/null +++ b/apps/server/src/rbac/rolemap.router.ts @@ -0,0 +1,59 @@ +import { Injectable } from '@nestjs/common'; +import { TrpcService } from '@server/trpc/trpc.service'; +import { RoleMapSchema } from '@nicestack/common'; +import { RoleMapService } from './rolemap.service'; + +@Injectable() +export class RoleMapRouter { + constructor( + private readonly trpc: TrpcService, + private readonly roleMapService: RoleMapService, + ) {} + + router = this.trpc.router({ + + deleteAllRolesForObject: this.trpc.protectProcedure + .input(RoleMapSchema.deleteWithObject) + .mutation(({ input }) => + this.roleMapService.deleteAllRolesForObject(input), + ), + + setRoleForObject: this.trpc.protectProcedure + .input(RoleMapSchema.create) + .mutation(({ input }) => this.roleMapService.setRoleForObject(input)), + + createManyObjects: this.trpc.protectProcedure + .input(RoleMapSchema.createManyObjects) + .mutation(({ input }) => this.roleMapService.createManyObjects(input)), + + setRolesForObject: this.trpc.protectProcedure + .input(RoleMapSchema.createManyRoles) + .mutation(({ input }) => this.roleMapService.setRolesForObject(input)), + + getPermsForObject: this.trpc.procedure + .input(RoleMapSchema.getPermsForObject) + .query(({ input }) => this.roleMapService.getPermsForObject(input)), + batchDelete: this.trpc.protectProcedure + .input(RoleMapSchema.batchDelete) // Assuming RoleMapSchema.batchDelete is the Zod schema for batch deleting staff + .mutation(async ({ input }) => { + return await this.roleMapService.batchDelete(input); + }), + + paginate: this.trpc.procedure + .input(RoleMapSchema.paginate) // Define the input schema for pagination + .query(async ({ input }) => { + return await this.roleMapService.paginate(input); + }), + update: this.trpc.protectProcedure + .input(RoleMapSchema.update) + .mutation(async ({ ctx, input }) => { + const { staff } = ctx; + return await this.roleMapService.update(input); + }), + getRoleMapDetail: this.trpc.procedure + .input(RoleMapSchema.getRoleMapDetail) + .query(async ({ input }) => { + return await this.roleMapService.getRoleMapDetail(input); + }), + }); +} diff --git a/apps/server/src/rbac/rolemap.service.ts b/apps/server/src/rbac/rolemap.service.ts new file mode 100644 index 0000000..ec5dc89 --- /dev/null +++ b/apps/server/src/rbac/rolemap.service.ts @@ -0,0 +1,215 @@ +import { Injectable } from '@nestjs/common'; +import { db, z, RoleMapSchema, ObjectType } from '@nicestack/common'; +import { DepartmentService } from '@server/models/department/department.service'; +import { TRPCError } from '@trpc/server'; + +@Injectable() +export class RoleMapService { + constructor(private readonly departmentService: DepartmentService) { } + + /** + * 删除某对象的所有角色 + * @param data 包含对象ID的数据 + * @returns 删除结果 + */ + async deleteAllRolesForObject( + data: z.infer, + ) { + const { objectId } = data; + return await db.roleMap.deleteMany({ + where: { + objectId, + }, + }); + } + + /** + * 为某对象设置一个角色 + * @param data 角色映射数据 + * @returns 创建的角色映射 + */ + async setRoleForObject(data: z.infer) { + return await db.roleMap.create({ + data, + }); + } + + /** + * 批量为多个对象创建角色映射 + * @param data 角色映射数据 + * @returns 创建的角色映射列表 + */ + async createManyObjects( + data: z.infer, + ) { + const { domainId, roleId, objectIds, objectType } = data; + const roleMaps = objectIds.map((id) => ({ + domainId, + objectId: id, + roleId, + objectType, + })); + + // 开启事务 + return await db.$transaction(async (prisma) => { + // 首先,删除现有的角色映射 + await prisma.roleMap.deleteMany({ + where: { + domainId, + roleId, + objectType, + }, + }); + // 然后,创建新的角色映射 + return await prisma.roleMap.createMany({ + data: roleMaps, + }); + }); + } + + /** + * 为某对象设置多个角色 + * @param data 角色映射数据 + * @returns 创建的角色映射列表 + */ + async setRolesForObject(data: z.infer) { + const { domainId, objectId, roleIds, objectType } = data; + const roleMaps = roleIds.map((id) => ({ + domainId, + objectId, + roleId: id, + objectType, + })); + + return await db.roleMap.createMany({ data: roleMaps }); + } + + /** + * 获取某对象的权限 + * @param data 包含域ID、部门ID和对象ID的数据 + * @returns 用户角色的权限列表 + */ + async getPermsForObject( + data: z.infer, + ) { + const { domainId, deptId, staffId } = data; + + let ancestorDeptIds = []; + if (deptId) { + ancestorDeptIds = + await this.departmentService.getAllParentDeptIds(deptId); + } + + const userRoles = await db.roleMap.findMany({ + where: { + AND: { + domainId, + OR: [ + { + objectId: staffId, + objectType: ObjectType.STAFF + }, + (deptId ? { + objectId: { in: [deptId, ...ancestorDeptIds] }, + objectType: ObjectType.DEPARTMENT, + } : {}), + ], + }, + }, + include: { role: true }, + }); + + return userRoles.flatMap((userRole) => userRole.role.permissions); + } + + /** + * 批量删除角色映射 + * @param data 包含要删除的角色映射ID列表的数据 + * @returns 删除结果 + * @throws 如果未提供ID,将抛出错误 + */ + async batchDelete(data: z.infer) { + const { ids } = data; + + if (!ids || ids.length === 0) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'No IDs provided for deletion.', + }); + } + + const deletedRoleMaps = await db.roleMap.deleteMany({ + where: { id: { in: ids } }, + }); + + if (!deletedRoleMaps.count) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: 'No taxonomies were found with the provided IDs.', + }); + } + + return { success: true, count: deletedRoleMaps.count }; + } + + /** + * 分页获取角色映射 + * @param data 包含分页信息的数据 + * @returns 分页结果,包含角色映射列表和总数 + */ + async paginate(data: z.infer) { + const { page, pageSize, domainId, roleId } = data; + + const [items, totalCount] = await Promise.all([ + db.roleMap.findMany({ + skip: (page - 1) * pageSize, + take: pageSize, + where: { domainId, roleId }, + }), + db.roleMap.count({ + where: { domainId, roleId }, + }), + ]); + + // const processedItems = await Promise.all(items.map(item => this.genRoleMapDto(item))); + return { items, totalCount }; + } + + /** + * 更新角色映射 + * @param data 包含更新信息的数据 + * @returns 更新后的角色映射 + */ + async update(data: z.infer) { + const { id, ...others } = data; + + // 开启事务 + return await db.$transaction(async (prisma) => { + // 更新角色映射 + const updatedRoleMap = await prisma.roleMap.update({ + where: { id }, + data: { ...others }, + }); + return updatedRoleMap; + }); + } + + /** + * 获取角色映射详情 + * @param data 包含角色ID和域ID的数据 + * @returns 角色映射详情,包含部门ID和员工ID列表 + */ + async getRoleMapDetail(data: z.infer) { + const { roleId, domainId } = data; + const res = await db.roleMap.findMany({ where: { roleId, domainId } }); + + const deptIds = res + .filter((item) => item.objectType === ObjectType.DEPARTMENT) + .map((item) => item.objectId); + const staffIds = res + .filter((item) => item.objectType === ObjectType.STAFF) + .map((item) => item.objectId); + + return { deptIds, staffIds }; + } +} diff --git a/apps/server/src/transform/transform.module.ts b/apps/server/src/transform/transform.module.ts index 898eef7..dc64a77 100644 --- a/apps/server/src/transform/transform.module.ts +++ b/apps/server/src/transform/transform.module.ts @@ -1,9 +1,10 @@ import { Module } from '@nestjs/common'; -import { TransformController } from './transform.controller'; import { TransformService } from './transform.service'; +import { TransformRouter } from './transform.router'; +import { TrpcService } from '@server/trpc/trpc.service'; @Module({ - controllers: [TransformController], - providers: [TransformService] + providers: [TransformService, TransformRouter, TrpcService], + exports: [TransformRouter] }) -export class TransformModule {} +export class TransformModule { } diff --git a/apps/server/src/trpc/trpc.module.ts b/apps/server/src/trpc/trpc.module.ts index 2ebd7a2..c974d3e 100644 --- a/apps/server/src/trpc/trpc.module.ts +++ b/apps/server/src/trpc/trpc.module.ts @@ -1,13 +1,17 @@ import { Module } from '@nestjs/common'; import { TrpcService } from './trpc.service'; import { TrpcRouter } from './trpc.router'; -import { HelloService } from '@server/hello/hello.service'; -import { HelloRouter } from '@server/hello/hello.router'; +import { DepartmentRouter } from '@server/models/department/department.router'; +import { TransformRouter } from '@server/transform/transform.router'; +import { StaffRouter } from '@server/models/staff/staff.router'; +import { StaffModule } from '../models/staff/staff.module'; +import { DepartmentModule } from '@server/models/department/department.module'; +import { TransformModule } from '@server/transform/transform.module'; @Module({ - imports: [], + imports: [StaffModule, DepartmentModule, TransformModule], controllers: [], - providers: [TrpcService, TrpcRouter, HelloRouter, HelloService], + providers: [TrpcService, TrpcRouter], }) export class TrpcModule { } diff --git a/apps/server/src/trpc/trpc.router.ts b/apps/server/src/trpc/trpc.router.ts index 032b8cd..f06997b 100644 --- a/apps/server/src/trpc/trpc.router.ts +++ b/apps/server/src/trpc/trpc.router.ts @@ -1,5 +1,4 @@ import { INestApplication, Injectable } from '@nestjs/common'; -import { AuthRouter } from '@server/auth/auth.router'; import { DepartmentRouter } from '@server/models/department/department.router'; import { StaffRouter } from '@server/models/staff/staff.router'; import { TrpcService } from '@server/trpc/trpc.service'; @@ -8,13 +7,11 @@ import { TransformRouter } from '../transform/transform.router'; @Injectable() export class TrpcRouter { constructor(private readonly trpc: TrpcService, - private readonly auth: AuthRouter, private readonly staff: StaffRouter, private readonly department: DepartmentRouter, private readonly transform: TransformRouter ) { } appRouter = this.trpc.router({ - auth: this.auth.router, staff: this.staff.router, department: this.department.router, transform: this.transform.router diff --git a/apps/server/src/trpc/trpc.service.ts b/apps/server/src/trpc/trpc.service.ts index 117e582..b6195b7 100644 --- a/apps/server/src/trpc/trpc.service.ts +++ b/apps/server/src/trpc/trpc.service.ts @@ -3,7 +3,7 @@ import { initTRPC, TRPCError } from '@trpc/server'; import superjson from 'superjson-cjs'; import * as trpcExpress from '@trpc/server/adapters/express'; import { env } from '@server/env'; -import { db, Staff, TokenPayload } from "@nicestack/common" +import { db, Staff, JwtPayload } from "@nicestack/common" import { JwtService } from '@nestjs/jwt'; type Context = Awaited>; @@ -15,15 +15,15 @@ export class TrpcService { res, }: trpcExpress.CreateExpressContextOptions) { const token = req.headers.authorization?.split(' ')[1]; - let tokenData: TokenPayload | undefined = undefined; + let tokenData: JwtPayload | undefined = undefined; let staff: Staff | undefined = undefined; if (token) { try { // Verify JWT token and extract tokenData - tokenData = await this.jwtService.verifyAsync(token, { secret: env.JWT_SECRET }) as TokenPayload; + tokenData = await this.jwtService.verifyAsync(token, { secret: env.JWT_SECRET }) as JwtPayload; if (tokenData) { // Fetch staff details from the database using tokenData.id - staff = await db.staff.findUnique({ where: { id: tokenData.id } }); + staff = await db.staff.findUnique({ where: { id: tokenData.sub } }); if (!staff) { throw new TRPCError({ code: 'UNAUTHORIZED', message: "User not found" }); } diff --git a/apps/web/index.html b/apps/web/index.html index e4b78ea..9bb5a6a 100644 --- a/apps/web/index.html +++ b/apps/web/index.html @@ -1,13 +1,22 @@ - - - - - Vite + React + TS - - -
- - - + + + + + + Vite + React + TS + + + + +
+ + + + \ No newline at end of file diff --git a/apps/web/package.json b/apps/web/package.json index 31cc865..9555266 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -10,22 +10,23 @@ "preview": "vite preview" }, "dependencies": { - "@tanstack/react-form": "^0.26.3", - "@tanstack/react-query": "^5.50.1", + "@nicestack/common": "workspace:^", "@tanstack/query-async-storage-persister": "^5.51.9", + "@tanstack/react-query": "^5.51.1", "@tanstack/react-query-persist-client": "^5.51.9", "@tanstack/react-virtual": "^3.8.3", "@tanstack/zod-form-adapter": "^0.26.3", "@trpc/client": "11.0.0-rc.456", "@trpc/react-query": "11.0.0-rc.456", "@trpc/server": "11.0.0-rc.456", + "axios": "^1.7.3", + "idb-keyval": "^6.2.1", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^6.24.1", "superjson": "^2.2.1", "zod": "^3.23.8", - "idb-keyval": "^6.2.1", - "@nicestack/common": "workspace:^" + "zustand": "^4.5.5" }, "devDependencies": { "@types/react": "^18.3.3", diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 73e61a3..edfd265 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -4,12 +4,15 @@ import { } from "react-router-dom"; import QueryProvider from './providers/query-provider' import { router } from './routes'; +import { AuthProvider } from './providers/auth-provider'; function App() { return ( - - - + + + + + ) } diff --git a/apps/web/src/app/login.tsx b/apps/web/src/app/login.tsx new file mode 100644 index 0000000..f9e1c16 --- /dev/null +++ b/apps/web/src/app/login.tsx @@ -0,0 +1,3 @@ +export default function LoginPage() { + return 'LoginPage' +} \ No newline at end of file diff --git a/apps/web/src/app/main/page.tsx b/apps/web/src/app/main/page.tsx index b7f1d2e..81a0c87 100644 --- a/apps/web/src/app/main/page.tsx +++ b/apps/web/src/app/main/page.tsx @@ -1,4 +1,3 @@ export default function MainPage() { return <>hello,world } - diff --git a/apps/web/src/components/auth/with-auth.tsx b/apps/web/src/components/auth/with-auth.tsx new file mode 100644 index 0000000..92667c8 --- /dev/null +++ b/apps/web/src/components/auth/with-auth.tsx @@ -0,0 +1,29 @@ +import { useAuth } from '@web/src/providers/auth-provider'; +import { RolePerms } from '@nicestack/common'; +import { ReactNode } from 'react'; +import { Navigate } from "react-router-dom"; +// Define a type for the props that the HOC will accept. +interface WithAuthProps { + permissions?: RolePerms[]; +} + +// Create the HOC function. +export default function WithAuth({ options = {}, children }: { children: ReactNode, options?: WithAuthProps }) { + const { isAuthenticated, user, isLoading } = useAuth(); + if (isLoading) { + return
Loading...
; + } + // If the user is not authenticated, redirect them to the login page. + if (!isAuthenticated) { + return + + } + if (options.permissions && user) { + const hasPermissions = options.permissions.every(permission => user.permissions.includes(permission)); + if (!hasPermissions) { + return
You do not have the required permissions to view this page.
; + } + } + // Return a new functional component. + return children +} diff --git a/apps/web/src/env.ts b/apps/web/src/env.ts new file mode 100644 index 0000000..e4f679f --- /dev/null +++ b/apps/web/src/env.ts @@ -0,0 +1,13 @@ +export const env: { + TUS_URL: string; + API_URL: string; +} = { + TUS_URL: + import.meta.env.PROD + ? (window as any).env.VITE_APP_TUS_URL + : import.meta.env.VITE_APP_TUS_URL, + API_URL: + import.meta.env.PROD + ? (window as any).env.VITE_APP_API_URL + : import.meta.env.VITE_APP_API_URL, +}; diff --git a/apps/web/src/providers/auth-provider.tsx b/apps/web/src/providers/auth-provider.tsx new file mode 100644 index 0000000..b0f0902 --- /dev/null +++ b/apps/web/src/providers/auth-provider.tsx @@ -0,0 +1,159 @@ +import React, { createContext, useContext, useState, useEffect, useCallback, ReactNode } from 'react'; +import apiClient from '../utils/axios-client'; +import { UserProfile } from '@nicestack/common'; + +interface AuthContextProps { + accessToken: string | null; + refreshToken: string | null; + isAuthenticated: boolean; + isLoading: boolean; + user: any; + login: (username: string, password: string) => Promise; + logout: () => Promise; + refreshAccessToken: () => Promise; + initializeAuth: () => void; + startTokenRefreshInterval: () => void; + fetchUserProfile: () => Promise; +} + +const AuthContext = createContext(undefined); +export const useAuth = (): AuthContextProps => { + const context = useContext(AuthContext); + if (!context) { + throw new Error('useAuth must be used within an AuthProvider'); + } + return context; +}; +interface AuthProviderProps { + children: ReactNode; +} +export const AuthProvider: React.FC = ({ children }) => { + const [accessToken, setAccessToken] = useState(localStorage.getItem('access_token')); + const [refreshToken, setRefreshToken] = useState(localStorage.getItem('refresh_token')); + const [isAuthenticated, setIsAuthenticated] = useState(!!localStorage.getItem('access_token')); + const [isLoading, setIsLoading] = useState(false); + const [intervalId, setIntervalId] = useState(null); + const [user, setUser] = useState(null); + const initializeAuth = useCallback(() => { + const storedAccessToken = localStorage.getItem('access_token'); + const storedRefreshToken = localStorage.getItem('refresh_token'); + setAccessToken(storedAccessToken); + setRefreshToken(storedRefreshToken); + setIsAuthenticated(!!storedAccessToken); + if (storedRefreshToken) { + startTokenRefreshInterval(); + } + if (storedAccessToken) { + fetchUserProfile(); + } + }, []); + const refreshAccessToken = useCallback(async () => { + if (!refreshToken) return; + try { + setIsLoading(true); + const response = await apiClient.post(`/auth/refresh-token`, { refreshToken }); + const { access_token, access_token_expires_at } = response.data; + localStorage.setItem('access_token', access_token); + localStorage.setItem('access_token_expires_at', access_token_expires_at); + setAccessToken(access_token); + setIsAuthenticated(true); + fetchUserProfile(); + } catch (err) { + console.error("Token refresh failed", err); + logout(); + } finally { + setIsLoading(false); + } + }, [refreshToken]); + + const startTokenRefreshInterval = useCallback(async () => { + if (intervalId) { + clearInterval(intervalId); + } + + await refreshAccessToken(); + + const newIntervalId = setInterval(refreshAccessToken, 10 * 60 * 1000); // 10 minutes + + setIntervalId(newIntervalId); + }, [intervalId, refreshAccessToken]); + + const login = async (username: string, password: string): Promise => { + try { + setIsLoading(true); + const response = await apiClient.post(`/auth/login`, { username, password }); + const { access_token, refresh_token, access_token_expires_at, refresh_token_expires_at } = response.data; + localStorage.setItem('access_token', access_token); + localStorage.setItem('refresh_token', refresh_token); + localStorage.setItem('access_token_expires_at', access_token_expires_at); + localStorage.setItem('refresh_token_expires_at', refresh_token_expires_at); + setAccessToken(access_token); + setRefreshToken(refresh_token); + setIsAuthenticated(true); + startTokenRefreshInterval(); + fetchUserProfile(); + } catch (err) { + console.error("Login failed", err); + throw new Error("Login failed"); + } finally { + setIsLoading(false); + } + }; + + const logout = async (): Promise => { + try { + setIsLoading(true); + const storedRefreshToken = localStorage.getItem('refresh_token'); + await apiClient.post(`/auth/logout`, { refreshToken: storedRefreshToken }); + + localStorage.removeItem('access_token'); + localStorage.removeItem('refresh_token'); + localStorage.removeItem('access_token_expires_at'); + localStorage.removeItem('refresh_token_expires_at'); + + setAccessToken(null); + setRefreshToken(null); + setIsAuthenticated(false); + setUser(null); + + if (intervalId) { + clearInterval(intervalId); + setIntervalId(null); + } + } catch (err) { + console.error("Logout failed", err); + throw new Error("Logout failed"); + } finally { + setIsLoading(false); + } + }; + + const fetchUserProfile = useCallback(async () => { + try { + const response = await apiClient.get(`/auth/user-profile`); + setUser(response.data); + } catch (err) { + console.error("Fetching user profile failed", err); + } + }, []); + + useEffect(() => { + initializeAuth(); + }, [initializeAuth]); + + const value: AuthContextProps = { + accessToken, + refreshToken, + isAuthenticated, + isLoading, + user, + login, + logout, + refreshAccessToken, + initializeAuth, + startTokenRefreshInterval, + fetchUserProfile + }; + + return {children}; +}; diff --git a/apps/web/src/providers/query-provider.tsx b/apps/web/src/providers/query-provider.tsx index f441268..e7eac82 100644 --- a/apps/web/src/providers/query-provider.tsx +++ b/apps/web/src/providers/query-provider.tsx @@ -1,25 +1,23 @@ import { QueryClient } from '@tanstack/react-query'; import { unstable_httpBatchStreamLink, loggerLink } from '@trpc/client'; -import React, { useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { api } from '../utils/trpc'; import superjson from 'superjson'; import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client' import { createIDBPersister } from '../utils/idb'; - +import { env } from '../env'; +import { useAuth } from './auth-provider'; export default function QueryProvider({ children }: { children: React.ReactNode }) { const [queryClient] = useState(() => new QueryClient()); - const [trpcClient] = useState(() => + const { accessToken } = useAuth() + const trpcClient = useMemo(() => api.createClient({ - links: [ unstable_httpBatchStreamLink({ - url: 'http://localhost:3000/trpc', - // You can pass any HTTP headers you wish here - async headers() { - return { - // authorization: getAuthCookie(), - }; - }, + url: `${env.API_URL}/trpc`, + headers: async () => ({ + ...(accessToken ? { 'Authorization': `Bearer ${accessToken}` } : {}), + }), transformer: superjson }), loggerLink({ @@ -29,7 +27,7 @@ export default function QueryProvider({ children }: { children: React.ReactNode (opts.direction === 'down' && opts.result instanceof Error), }), ], - }), + }), [accessToken] ); return ( diff --git a/apps/web/src/routes/index.tsx b/apps/web/src/routes/index.tsx index 989cb98..0e626b5 100644 --- a/apps/web/src/routes/index.tsx +++ b/apps/web/src/routes/index.tsx @@ -4,6 +4,8 @@ import { import MainPage from "../app/main/page"; import ErrorPage from "../app/error"; import LayoutPage from "../app/layout"; +import LoginPage from "../app/login"; +import WithAuth from "../components/auth/with-auth"; export const router = createBrowserRouter([ { @@ -13,8 +15,13 @@ export const router = createBrowserRouter([ children: [ { index: true, - element: + element: } - ] + ], }, + { + path: '/login', + element: + } + ]); diff --git a/apps/web/src/utils/axios-client.ts b/apps/web/src/utils/axios-client.ts new file mode 100644 index 0000000..da8905c --- /dev/null +++ b/apps/web/src/utils/axios-client.ts @@ -0,0 +1,20 @@ +import axios from 'axios'; +import { env } from '../env'; +const BASE_URL = env.API_URL; // Replace with your backend URL +const apiClient = axios.create({ + baseURL: BASE_URL, + withCredentials: true, +}); +// Add a request interceptor to attach the access token +apiClient.interceptors.request.use( + (config) => { + const accessToken = localStorage.getItem('access_token'); + if (accessToken) { + config.headers['Authorization'] = `Bearer ${accessToken}`; + } + return config; + }, + (error) => Promise.reject(error) +); + +export default apiClient; diff --git a/packages/common/prisma/schema.prisma b/packages/common/prisma/schema.prisma index cb8d4df..fd936a4 100755 --- a/packages/common/prisma/schema.prisma +++ b/packages/common/prisma/schema.prisma @@ -71,18 +71,17 @@ model TermAncestry { } model Comment { - id String @id @default(uuid()) - style String - link String? - title String? - content String - attachments String[] @default([]) - createdAt DateTime? @default(now()) - updatedAt DateTime @updatedAt - deletedAt DateTime? - createdBy String? - - createdStaff Staff? @relation(fields: [createdBy], references: [id]) + id String @id @default(uuid()) + style String + link String? + title String? + content String + attachments String[] @default([]) + createdAt DateTime? @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime? + createdBy String? + createdStaff Staff? @relation(fields: [createdBy], references: [id]) @@map("comments") } diff --git a/packages/common/src/enum.ts b/packages/common/src/enum.ts index 67105c0..723d9d0 100644 --- a/packages/common/src/enum.ts +++ b/packages/common/src/enum.ts @@ -9,6 +9,8 @@ export enum RelationType { READED = "READED", MESSAGE = "MESSAGE", } + + export enum RolePerms { // Create Permissions 创建权限 CREATE_ALERT = "CREATE_ALERT", // 创建警报 diff --git a/packages/common/src/schema.ts b/packages/common/src/schema.ts index d1e48e6..b10a296 100755 --- a/packages/common/src/schema.ts +++ b/packages/common/src/schema.ts @@ -1,4 +1,5 @@ import { z } from "zod" +import { ObjectType } from "./enum"; export const AuthSchema = { signInRequset: z.object({ username: z.string(), @@ -18,7 +19,7 @@ export const AuthSchema = { }), logoutRequest: z.object({ refreshToken: z.string(), - }), + }) }; export const StaffSchema = { create: z.object({ @@ -90,3 +91,73 @@ export const DepartmentSchema = { ids: z.array(z.string()).nullish(), }), }; +export const RoleMapSchema = { + create: z.object({ + objectId: z.string(), + roleId: z.string(), + domainId: z.string(), + objectType: z.nativeEnum(ObjectType), + }), + update: z.object({ + id: z.string(), + objectId: z.string().nullish(), + roleId: z.string().nullish(), + domainId: z.string().nullish(), + objectType: z.nativeEnum(ObjectType).nullish(), + }), + createManyRoles: z.object({ + objectId: z.string(), + roleIds: z.array(z.string()), + domainId: z.string(), + objectType: z.nativeEnum(ObjectType), + }), + createManyObjects: z.object({ + objectIds: z.array(z.string()), + roleId: z.string(), + domainId: z.string().nullish(), + objectType: z.nativeEnum(ObjectType), + }), + batchDelete: z.object({ + ids: z.array(z.string()), + }), + paginate: z.object({ + page: z.number().min(1), + pageSize: z.number().min(1), + domainId: z.string().nullish(), + roleId: z.string().nullish(), + }), + deleteWithObject: z.object({ + objectId: z.string(), + }), + + getRoleMapDetail: z.object({ + roleId: z.string(), + domainId: z.string().nullish(), + }), + getPermsForObject: z.object({ + domainId: z.string(), + staffId: z.string(), + deptId: z.string(), + }), +}; +export const RoleSchema = { + create: z.object({ + name: z.string(), + permissions: z.array(z.string()).nullish(), + }), + update: z.object({ + id: z.string(), + name: z.string().nullish(), + permissions: z.array(z.string()).nullish(), + }), + batchDelete: z.object({ + ids: z.array(z.string()), + }), + paginate: z.object({ + page: z.number().nullish(), + pageSize: z.number().nullish(), + }), + findMany: z.object({ + keyword: z.string().nullish(), + }), +}; diff --git a/packages/common/src/type.ts b/packages/common/src/type.ts index e8d68d5..8271fb4 100644 --- a/packages/common/src/type.ts +++ b/packages/common/src/type.ts @@ -13,7 +13,29 @@ export type StaffDto = Staff & { domain?: Department; department?: Department; }; -export interface TokenPayload { - id: string; +export type UserProfile = Staff & { + permissions: string[]; + department?: Department; + domain?: Department; +} + +export interface JwtPayload { + sub: string; username: string; +} +export interface GenPerms { + instruction?: boolean; + createProgress?: boolean; + requestCancel?: boolean; + acceptCancel?: boolean; + + conclude?: boolean; + createRisk?: boolean; + editIndicator?: boolean; + editMethod?: boolean; + editOrg?: boolean; + + edit?: boolean; + delete?: boolean; + read?: boolean; } \ No newline at end of file