diff --git a/apps/server/package.json b/apps/server/package.json index 5c048ee..7aae1df 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -24,12 +24,18 @@ "@nestjs/common": "^10.3.10", "@nestjs/config": "^3.2.3", "@nestjs/core": "^10.0.0", + "@nestjs/jwt": "^10.2.0", "@nestjs/platform-express": "^10.0.0", + "@nestjs/schedule": "^4.1.0", "@nestjs/websockets": "^10.3.10", "@nicestack/common": "workspace:^", "@trpc/server": "11.0.0-rc.456", "axios": "^1.7.3", + "bcrypt": "^5.1.1", "bullmq": "^5.12.0", + "cron": "^3.1.7", + "dayjs": "^1.11.13", + "exceljs": "^4.4.0", "ioredis": "^5.4.1", "mime-types": "^2.1.35", "reflect-metadata": "^0.2.0", @@ -37,21 +43,20 @@ "socket.io": "^4.7.5", "superjson-cjs": "^2.2.3", "tus-js-client": "^4.1.0", - "zod": "^3.23.8", - "dayjs": "^1.11.13", - "exceljs": "^4.4.0" + "zod": "^3.23.8" }, "devDependencies": { "@nestjs/cli": "^10.0.0", "@nestjs/schematics": "^10.0.0", "@nestjs/testing": "^10.0.0", - "@types/jest": "^29.5.2", - "@types/mime-types": "^2.1.4", - "@types/node": "^20.3.1", - "@types/supertest": "^6.0.0", + "@types/bcrypt": "^5.0.2", "@types/exceljs": "^1.3.0", "@types/express": "^4.17.21", + "@types/jest": "^29.5.2", + "@types/mime-types": "^2.1.4", "@types/multer": "^1.4.12", + "@types/node": "^20.3.1", + "@types/supertest": "^6.0.0", "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", "eslint": "^8.42.0", diff --git a/apps/server/src/app.controller.spec.ts b/apps/server/src/app.controller.spec.ts deleted file mode 100644 index d22f389..0000000 --- a/apps/server/src/app.controller.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { AppController } from './app.controller'; -import { AppService } from './app.service'; - -describe('AppController', () => { - let appController: AppController; - - beforeEach(async () => { - const app: TestingModule = await Test.createTestingModule({ - controllers: [AppController], - providers: [AppService], - }).compile(); - - appController = app.get(AppController); - }); - - describe('root', () => { - it('should return "Hello World!"', () => { - expect(appController.getHello()).toBe('Hello World!'); - }); - }); -}); diff --git a/apps/server/src/app.controller.ts b/apps/server/src/app.controller.ts deleted file mode 100644 index cce879e..0000000 --- a/apps/server/src/app.controller.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Controller, Get } from '@nestjs/common'; -import { AppService } from './app.service'; - -@Controller() -export class AppController { - constructor(private readonly appService: AppService) {} - - @Get() - getHello(): string { - return this.appService.getHello(); - } -} diff --git a/apps/server/src/app.module.ts b/apps/server/src/app.module.ts index 6899d13..1559af9 100644 --- a/apps/server/src/app.module.ts +++ b/apps/server/src/app.module.ts @@ -1,6 +1,4 @@ import { Module } from '@nestjs/common'; -import { AppController } from './app.controller'; -import { AppService } from './app.service'; import { TrpcModule } from './trpc/trpc.module'; import { RedisService } from './redis/redis.service'; @@ -8,11 +6,11 @@ import { RedisModule } from './redis/redis.module'; import { SocketGateway } from './socket/socket.gateway'; import { QueueModule } from './queue/queue.module'; import { TransformModule } from './transform/transform.module'; -import { ControllerService } from './controller/controller.service'; +import { AuthModule } from './auth/auth.module'; +import { ScheduleModule } from '@nestjs/schedule'; @Module({ - imports: [TrpcModule, RedisModule, QueueModule, TransformModule], - controllers: [AppController], - providers: [AppService, RedisService, SocketGateway, ControllerService], + imports: [ScheduleModule.forRoot(), TrpcModule, RedisModule, QueueModule, TransformModule, AuthModule], + providers: [RedisService, SocketGateway], }) export class AppModule { } diff --git a/apps/server/src/app.service.ts b/apps/server/src/app.service.ts deleted file mode 100644 index 927d7cc..0000000 --- a/apps/server/src/app.service.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -@Injectable() -export class AppService { - getHello(): string { - return 'Hello World!'; - } -} diff --git a/apps/server/src/auth/auth.guard.ts b/apps/server/src/auth/auth.guard.ts new file mode 100644 index 0000000..a2bd1d3 --- /dev/null +++ b/apps/server/src/auth/auth.guard.ts @@ -0,0 +1,40 @@ +import { + CanActivate, + ExecutionContext, + Injectable, + UnauthorizedException, +} from '@nestjs/common'; +import { JwtService } from '@nestjs/jwt'; +import { env } from '@server/env'; +import { Request } from 'express'; + +@Injectable() +export class AuthGuard implements CanActivate { + constructor(private jwtService: JwtService) { } + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + const token = this.extractTokenFromHeader(request); + if (!token) { + throw new UnauthorizedException(); + } + try { + const payload = await this.jwtService.verifyAsync( + token, + { + secret: env.JWT_SECRET + } + ); + // 💡 We're assigning the payload to the request object here + // so that we can access it in our route handlers + request['user'] = payload; + } catch { + throw new UnauthorizedException(); + } + return true; + } + + private extractTokenFromHeader(request: Request): string | undefined { + const [type, token] = request.headers.authorization?.split(' ') ?? []; + return type === 'Bearer' ? token : undefined; + } +} \ No newline at end of file diff --git a/apps/server/src/auth/auth.module.ts b/apps/server/src/auth/auth.module.ts new file mode 100644 index 0000000..d60f0a9 --- /dev/null +++ b/apps/server/src/auth/auth.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { AuthService } from './auth.service'; +import { JwtModule } from '@nestjs/jwt'; +import { env } from '@server/env'; + +@Module({ + providers: [AuthService], + imports: [JwtModule.register({ + global: true, + secret: env.JWT_SECRET, + signOptions: { expiresIn: '60s' }, + }),] +}) +export class AuthModule { } diff --git a/apps/server/src/auth/auth.router.ts b/apps/server/src/auth/auth.router.ts new file mode 100644 index 0000000..cd5e414 --- /dev/null +++ b/apps/server/src/auth/auth.router.ts @@ -0,0 +1,27 @@ +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 new file mode 100644 index 0000000..c5d9092 --- /dev/null +++ b/apps/server/src/auth/auth.service.ts @@ -0,0 +1,120 @@ +import { + 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'; + +@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); + + 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 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; + try { + payload = this.jwtService.verify(refreshToken); + } catch (error) { + throw new UnauthorizedException('Invalid refresh token'); + } + + const storedToken = await db.refreshToken.findUnique({ where: { token: refreshToken } }); + + if (!storedToken) { + throw new UnauthorizedException('Invalid refresh token'); + } + + const user = await db.staff.findUnique({ where: { id: payload.sub } }); + if (!user) { + throw new UnauthorizedException('Invalid refresh token'); + } + + const newAccessToken = await this.jwtService.signAsync( + { sub: user.id, username: user.username }, + { expiresIn: '1h' }, + ); + + return { + access_token: newAccessToken, + }; + } + + 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' }; + } +} diff --git a/apps/server/src/env.ts b/apps/server/src/env.ts new file mode 100755 index 0000000..74aecae --- /dev/null +++ b/apps/server/src/env.ts @@ -0,0 +1,3 @@ +export const env: { JWT_SECRET: string } = { + JWT_SECRET: process.env.JWT_SECRET || '/yT9MnLm/r6NY7ee2Fby6ihCHZl+nFx4OQFKupivrhA=' +} \ No newline at end of file diff --git a/apps/server/src/hello/hello.router.ts b/apps/server/src/hello/hello.router.ts deleted file mode 100644 index c26196f..0000000 --- a/apps/server/src/hello/hello.router.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { TrpcService } from '@server/trpc/trpc.service'; -import { HelloService } from './hello.service'; - -@Injectable() -export class HelloRouter { - constructor(private readonly trpc: TrpcService, private readonly hello: HelloService) { } - router = this.trpc.router({ - hello: this.trpc.procedure.query(() => this.hello.helloWorld()), - }); -} - diff --git a/apps/server/src/hello/hello.service.ts b/apps/server/src/hello/hello.service.ts deleted file mode 100644 index 960230f..0000000 --- a/apps/server/src/hello/hello.service.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -@Injectable() -export class HelloService { - helloWorld() { - return { - greeting: `Hello world`, - }; - } -} diff --git a/apps/server/src/init/init.module.ts b/apps/server/src/init/init.module.ts new file mode 100644 index 0000000..8113776 --- /dev/null +++ b/apps/server/src/init/init.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { InitService } from './init.service'; + +@Module({ + providers: [InitService], + exports: [InitService] +}) +export class InitModule { } diff --git a/apps/server/src/init/init.service.ts b/apps/server/src/init/init.service.ts new file mode 100644 index 0000000..653f0f6 --- /dev/null +++ b/apps/server/src/init/init.service.ts @@ -0,0 +1,90 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { db, InitRoles, InitTaxonomies, ObjectType } from "@nicestack/common"; +import { AuthService } from '@server/auth/auth.service'; + +@Injectable() +export class InitService { + private readonly logger = new Logger(InitService.name); + constructor(private readonly authService: AuthService) { } + private async createRoles() { + this.logger.log('Checking existing system roles'); + + for (const role of InitRoles) { + const existingRole = await db.role.findUnique({ + where: { name: role.name }, + }); + + if (!existingRole) { + this.logger.log(`Creating role: ${role.name}`); + await db.role.create({ + data: { ...role, system: true }, + }); + } else { + this.logger.log(`Role already exists: ${role.name}`); + } + } + } + private async createTaxonomy() { + this.logger.log('Checking existing taxonomies'); + + const existingTaxonomies = await db.taxonomy.findMany(); + const existingTaxonomyNames = existingTaxonomies.map(taxonomy => taxonomy.name); + + for (const [index, taxonomy] of InitTaxonomies.entries()) { + if (!existingTaxonomyNames.includes(taxonomy.name)) { + this.logger.log(`Creating taxonomy: ${taxonomy.name}`); + await db.taxonomy.create({ + data: { + ...taxonomy, + order: index, + }, + }); + } else { + this.logger.log(`Taxonomy already exists: ${taxonomy.name}`); + } + } + } + + private async createRoot() { + this.logger.log('Checking for root account'); + const rootAccountExists = await db.staff.findUnique({ + where: { phoneNumber: process.env.ADMIN_PHONE_NUMBER || '000000' }, + }); + if (!rootAccountExists) { + this.logger.log('Creating root account'); + const rootStaff =await this.authService.signUp({ + username: 'root', + password: 'admin' + }) + const rootRole = await db.role.findUnique({ + where: { name: '根管理员' }, + }); + + if (rootRole) { + this.logger.log('Assigning root role to root account'); + await db.roleMap.create({ + data: { + objectType: ObjectType.STAFF, + objectId: rootStaff.id, + roleId: rootRole.id, + }, + }); + } else { + this.logger.error('Root role does not exist'); + } + } else { + this.logger.log('Root account already exists'); + } + } + + async init() { + this.logger.log('Initializing system roles'); + await this.createRoles(); + + this.logger.log('Initializing root account'); + await this.createRoot(); + + this.logger.log('Initializing taxonomies'); + await this.createTaxonomy(); + } +} diff --git a/apps/server/src/models/department/department.module.ts b/apps/server/src/models/department/department.module.ts new file mode 100755 index 0000000..99cc160 --- /dev/null +++ b/apps/server/src/models/department/department.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { DepartmentService } from './department.service'; +import { DepartmentRouter } from './department.router'; +import { TrpcService } from '@server/trpc/trpc.service'; + +@Module({ + providers: [DepartmentService, DepartmentRouter, TrpcService], + exports: [DepartmentService, DepartmentRouter] +}) +export class DepartmentModule { } diff --git a/apps/server/src/models/department/department.router.ts b/apps/server/src/models/department/department.router.ts new file mode 100755 index 0000000..a7293b2 --- /dev/null +++ b/apps/server/src/models/department/department.router.ts @@ -0,0 +1,67 @@ +import { Injectable } from '@nestjs/common'; +import { TrpcService } from '@server/trpc/trpc.service'; +import { DepartmentService } from './department.service'; // assuming it's in the same directory +import { DepartmentSchema, z } from '@nicestack/common'; + +@Injectable() +export class DepartmentRouter { + constructor( + private readonly trpc: TrpcService, + private readonly departmentService: DepartmentService, // inject DepartmentService + ) {} + + router = this.trpc.router({ + create: this.trpc.protectProcedure + .input(DepartmentSchema.create) // expect input according to the schema + .mutation(async ({ input }) => { + return this.departmentService.create(input); + }), + update: this.trpc.protectProcedure + .input(DepartmentSchema.update) // expect input according to the schema + .mutation(async ({ input }) => { + return this.departmentService.update(input); + }), + + delete: this.trpc.protectProcedure + .input(DepartmentSchema.delete) // expect input according to the schema + .mutation(async ({ input }) => { + return this.departmentService.delete(input); + }), + getDepartmentDetails: this.trpc.procedure + .input(z.object({ deptId: z.string() })) // expect an object with deptId + .query(async ({ input }) => { + return this.departmentService.getDepartmentDetails(input.deptId); + }), + getAllChildDeptIds: this.trpc.procedure + .input(z.object({ deptId: z.string() })) // expect an object with deptId + .query(async ({ input }) => { + return this.departmentService.getAllChildDeptIds(input.deptId); + }), + getAllParentDeptIds: this.trpc.procedure + .input(z.object({ deptId: z.string() })) // expect an object with deptId + .query(async ({ input }) => { + return this.departmentService.getAllParentDeptIds(input.deptId); + }), + getChildren: this.trpc.procedure + .input(z.object({ parentId: z.string().nullish() })) + .query(async ({ input }) => { + return this.departmentService.getChildren(input.parentId); + }), + getDomainDepartments: this.trpc.procedure + .input(z.object({ query: z.string().nullish() })) + .query(async ({ input }) => { + const { query } = input; + return this.departmentService.getDomainDepartments(query); + }), + findMany: this.trpc.procedure + .input(DepartmentSchema.findMany) // Assuming StaffSchema.findMany is the Zod schema for finding staffs by keyword + .query(async ({ input }) => { + return await this.departmentService.findMany(input); + }), + paginate: this.trpc.procedure + .input(DepartmentSchema.paginate) // Assuming StaffSchema.findMany is the Zod schema for finding staffs by keyword + .query(async ({ input }) => { + return await this.departmentService.paginate(input); + }), + }); +} diff --git a/apps/server/src/models/department/department.service.ts b/apps/server/src/models/department/department.service.ts new file mode 100755 index 0000000..63cbe4a --- /dev/null +++ b/apps/server/src/models/department/department.service.ts @@ -0,0 +1,365 @@ +import { Injectable } from '@nestjs/common'; +import { db, z, DepartmentSchema } from '@nicestack/common'; + +@Injectable() +export class DepartmentService { + /** + * 获取某单位的所有子单位(平铺结构)。 + * @param deptId - 单位的唯一标识符。 + * @returns 包含所有子单位的数组,如果未传递deptId则返回undefined。 + */ + async getFlatChildDepts(deptId: string) { + if (!deptId) return undefined; + + return await db.deptAncestry.findMany({ + where: { ancestorId: deptId }, + }); + } + /** + * 获取指定DOM下的对应name的单位 + * @param domId + * @param name + * @returns + * + * + */ + + async findByNameInDom(domId: string, name: string) { + const subDepts = await db.deptAncestry.findMany({ + where: { + ancestorId: domId, + }, + include: { + descendant: true, + }, + }); + const dept = subDepts.find((item) => item.descendant.name === name); + + return dept?.descendant; + } + /** + * 获取某单位的所有父单位(平铺结构)。 + * @param deptId - 单位的唯一标识符。 + * @returns 包含所有父单位的数组,如果未传递deptId则返回undefined。 + */ + async getFlatParentDepts(deptId: string) { + if (!deptId) return undefined; + + return await db.deptAncestry.findMany({ + where: { descendantId: deptId }, + }); + } + + /** + * 获取某单位的所有子单位ID。 + * @param deptId - 单位的唯一标识符。 + * @returns 包含所有子单位ID的数组。 + */ + async getAllChildDeptIds(deptId: string) { + const res = await this.getFlatChildDepts(deptId); + return res?.map((dept) => dept.descendantId) || []; + } + + /** + * 获取某单位的所有父单位ID。 + * @param deptId - 单位的唯一标识符。 + * @returns 包含所有父单位ID的数组。 + */ + async getAllParentDeptIds(deptId: string) { + const res = await this.getFlatParentDepts(deptId); + return res?.map((dept) => dept.ancestorId) || []; + } + + /** + * 获取单位及其直接子单位的详细信息。 + * @param deptId - 要获取的单位的唯一标识符。 + * @returns 包含单位详细信息的对象,包括其子单位和员工信息。 + */ + async getDepartmentDetails(deptId: string) { + const department = await db.department.findUnique({ + where: { id: deptId }, + include: { children: true, deptStaffs: true }, + }); + + const childrenData = await db.deptAncestry.findMany({ + where: { ancestorId: deptId, relDepth: 1 }, + include: { descendant: { include: { children: true } } }, + }); + + const children = childrenData.map(({ descendant }) => ({ + id: descendant.id, + name: descendant.name, + order: descendant.order, + parentId: descendant.parentId, + hasChildren: Boolean(descendant.children?.length), + childrenCount: descendant.children?.length || 0, + })); + + return { + id: department?.id, + name: department?.name, + order: department?.order, + parentId: department?.parentId, + children, + staffs: department?.deptStaffs, + hasChildren: !!children.length, + }; + } + + /** + * 获取某单位的所有直接子单位。 + * @param parentId - 父单位的唯一标识符,如果未传递则获取顶级单位。 + * @returns 包含所有直接子单位信息的数组。 + */ + async getChildren(parentId?: string) { + const departments = await db.department.findMany({ + where: { parentId: parentId ?? null }, + include: { children: true, deptStaffs: true }, + }); + + return departments.map((dept) => ({ + ...dept, + hasChildren: dept.children.length > 0, + staffs: dept.deptStaffs, + })); + } + async paginate(data: z.infer) { + const { page, pageSize, ids } = data; + + const [items, totalCount] = await Promise.all([ + db.department.findMany({ + skip: (page - 1) * pageSize, + take: pageSize, + where: { + deletedAt: null, + OR: [{ id: { in: ids } }], + }, + include: { deptStaffs: true, parent: true }, + orderBy: { order: 'asc' }, + }), + db.department.count({ + where: { + deletedAt: null, + OR: [{ id: { in: ids } }], + }, + }), + ]); + + return { items, totalCount }; + } + async findMany(data: z.infer) { + const { keyword = '', ids } = data; + + const departments = await db.department.findMany({ + where: { + deletedAt: null, + OR: [{ name: { contains: keyword! } }, ids ? { id: { in: ids } } : {}], + }, + include: { deptStaffs: true }, + orderBy: { order: 'asc' }, + take: 20, + }); + + return departments.map((dept) => ({ + ...dept, + staffs: dept.deptStaffs, + })); + } + + /** + * 获取所有域内单位,根据查询条件筛选结果。 + * @param query - 可选的查询条件,用于模糊匹配单位名称。 + * @returns 包含符合条件的域内单位信息的数组。 + */ + async getDomainDepartments(query?: string) { + return await db.department.findMany({ + where: { isDomain: true, name: { contains: query } }, + take: 10, + }); + } + + async getDeptIdsByStaffIds(ids: string[]) { + const staffs = await db.staff.findMany({ + where: { id: { in: ids } }, + }); + + return staffs.map((staff) => staff.deptId); + } + + /** + * 创建一个新的单位并管理DeptAncestry关系。 + * @param data - 用于创建新单位的数据。 + * @returns 新创建的单位对象。 + */ + async create(data: z.infer) { + let newOrder = 0; + + // 确定新单位的顺序 + const siblingDepartments = await db.department.findMany({ + where: { parentId: data.parentId ?? null }, + orderBy: { order: 'desc' }, + take: 1, + }); + + if (siblingDepartments.length > 0) { + newOrder = siblingDepartments[0].order + 1; + } + + // 根据计算的顺序创建新单位 + const newDepartment = await db.department.create({ + data: { ...data, order: newOrder }, + }); + + // 如果存在parentId,则更新DeptAncestry关系 + if (data.parentId) { + const parentAncestries = await db.deptAncestry.findMany({ + where: { descendantId: data.parentId }, + orderBy: { relDepth: 'asc' }, + }); + + // 为新单位创建新的祖先记录 + const newAncestries = parentAncestries.map((ancestry) => ({ + ancestorId: ancestry.ancestorId, + descendantId: newDepartment.id, + relDepth: ancestry.relDepth + 1, + })); + + newAncestries.push({ + ancestorId: data.parentId, + descendantId: newDepartment.id, + relDepth: 1, + }); + + await db.deptAncestry.createMany({ data: newAncestries }); + } + + return newDepartment; + } + + /** + * 更新现有单位,并在parentId更改时管理DeptAncestry关系。 + * @param data - 用于更新现有单位的数据。 + * @returns 更新后的单位对象。 + */ + async update(data: z.infer) { + return await db.$transaction(async (transaction) => { + const currentDepartment = await transaction.department.findUnique({ + where: { id: data.id }, + }); + if (!currentDepartment) throw new Error('Department not found'); + + const updatedDepartment = await transaction.department.update({ + where: { id: data.id }, + data: data, + }); + + if (data.parentId !== currentDepartment.parentId) { + await transaction.deptAncestry.deleteMany({ + where: { descendantId: data.id }, + }); + + if (data.parentId) { + const parentAncestries = await transaction.deptAncestry.findMany({ + where: { descendantId: data.parentId }, + }); + + const newAncestries = parentAncestries.map((ancestry) => ({ + ancestorId: ancestry.ancestorId, + descendantId: data.id, + relDepth: ancestry.relDepth + 1, + })); + + newAncestries.push({ + ancestorId: data.parentId, + descendantId: data.id, + relDepth: 1, + }); + + await transaction.deptAncestry.createMany({ data: newAncestries }); + } + } + + return updatedDepartment; + }); + } + + /** + * 删除现有单位并清理DeptAncestry关系。 + * @param data - 用于删除现有单位的数据。 + * @returns 删除的单位对象。 + */ + async delete(data: z.infer) { + const deletedDepartment = await db.department.update({ + where: { id: data.id }, + data: { deletedAt: new Date() }, + }); + + await db.deptAncestry.deleteMany({ + where: { OR: [{ ancestorId: data.id }, { descendantId: data.id }] }, + }); + return deletedDepartment; + } + async getStaffsByDeptIds(ids: string[]) { + const depts = await db.department.findMany({ + where: { id: { in: ids } }, + include: { deptStaffs: true }, + }); + return depts.flatMap((dept) => dept.deptStaffs); + } + /** + * 获取指定部门及其所有子部门的员工。 + * @param deptIds - 要获取员工ID的部门ID数组。 + * @returns 包含所有员工ID的数组。 + */ + async getAllStaffsByDepts(deptIds: string[]) { + const allDeptIds = new Set(deptIds); + for (const deptId of deptIds) { + const childDeptIds = await this.getAllChildDeptIds(deptId); + childDeptIds.forEach((id) => allDeptIds.add(id)); + } + return await this.getStaffsByDeptIds(Array.from(allDeptIds)); + } + + /** + * 根据部门名称和域ID获取部门ID。 + * + * @param {string} name - 部门名称。 + * @param {string} domainId - 域标识符。 + * @returns {Promise} - 如果找到则返回部门ID,否则返回null。 + */ + async getDeptIdByName(name: string, domainId: string): Promise { + const dept = await db.department.findFirst({ + where: { + name, + ancestors: { + some: { + ancestorId: domainId + } + } + } + }); + return dept ? dept.id : null; + } + + /** + * 根据部门名称列表和域ID获取多个部门的ID。 + * + * @param {string[]} names - 部门名称列表。 + * @param {string} domainId - 域标识符。 + * @returns {Promise>} - 一个从部门名称到对应ID或null的记录。 + */ + async getDeptIdsByNames(names: string[], domainId: string): Promise> { + const result: Record = {}; + + // 遍历每个部门名称并获取对应的部门ID + for (const name of names) { + // 使用之前定义的函数根据名称获取部门ID + const deptId = await this.getDeptIdByName(name, domainId); + result[name] = deptId; + } + + return result; + } + + +} diff --git a/apps/server/src/models/staff/staff.module.ts b/apps/server/src/models/staff/staff.module.ts new file mode 100644 index 0000000..871eced --- /dev/null +++ b/apps/server/src/models/staff/staff.module.ts @@ -0,0 +1,4 @@ +import { Module } from '@nestjs/common'; + +@Module({}) +export class StaffModule {} diff --git a/apps/server/src/models/staff/staff.router.ts b/apps/server/src/models/staff/staff.router.ts new file mode 100755 index 0000000..a0196aa --- /dev/null +++ b/apps/server/src/models/staff/staff.router.ts @@ -0,0 +1,53 @@ +import { Injectable } from '@nestjs/common'; +import { TrpcService } from '@server/trpc/trpc.service'; +import { StaffService } from './staff.service'; // Adjust the import path as necessary +import { z, StaffSchema } from '@nicestack/common'; + +@Injectable() +export class StaffRouter { + constructor( + private readonly trpc: TrpcService, + private readonly staffService: StaffService, + ) {} + + router = this.trpc.router({ + create: this.trpc.procedure + .input(StaffSchema.create) // Assuming StaffSchema.create is the Zod schema for creating staff + .mutation(async ({ input }) => { + return await this.staffService.create(input); + }), + + update: this.trpc.procedure + .input(StaffSchema.update) // Assuming StaffSchema.update is the Zod schema for updating staff + .mutation(async ({ input }) => { + return await this.staffService.update(input); + }), + + batchDelete: this.trpc.procedure + .input(StaffSchema.batchDelete) // Assuming StaffSchema.batchDelete is the Zod schema for batch deleting staff + .mutation(async ({ input }) => { + return await this.staffService.batchDelete(input); + }), + + paginate: this.trpc.procedure + .input(StaffSchema.paginate) // Define the input schema for pagination + .query(async ({ input }) => { + return await this.staffService.paginate(input); + }), + findByDept: this.trpc.procedure + .input(StaffSchema.findByDept) + .query(async ({ input }) => { + return await this.staffService.findByDept(input); + }), + findMany: this.trpc.procedure + .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/hello/hello.service.spec.ts b/apps/server/src/models/staff/staff.service.spec.ts similarity index 56% rename from apps/server/src/hello/hello.service.spec.ts rename to apps/server/src/models/staff/staff.service.spec.ts index edfe4f9..d653df4 100644 --- a/apps/server/src/hello/hello.service.spec.ts +++ b/apps/server/src/models/staff/staff.service.spec.ts @@ -1,15 +1,15 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { HelloService } from './hello.service'; +import { StaffService } from './staff.service'; -describe('HelloService', () => { - let service: HelloService; +describe('StaffService', () => { + let service: StaffService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [HelloService], + providers: [StaffService], }).compile(); - service = module.get(HelloService); + service = module.get(StaffService); }); it('should be defined', () => { diff --git a/apps/server/src/models/staff/staff.service.ts b/apps/server/src/models/staff/staff.service.ts new file mode 100644 index 0000000..20dcac2 --- /dev/null +++ b/apps/server/src/models/staff/staff.service.ts @@ -0,0 +1,178 @@ +import { Injectable } from '@nestjs/common'; +import { db, ObjectType, Staff, StaffSchema, z } from '@nicestack/common'; +import { TRPCError } from '@trpc/server'; +import { DepartmentService } from '../department/department.service'; +@Injectable() +export class StaffService { + constructor(private readonly departmentService: DepartmentService) { } + + /** + * 获取某一单位下所有staff的记录 + * @param deptId 单位的id + * @returns 查到的staff记录 + */ + async findByDept(data: z.infer) { + const { deptId, domainId } = data; + const childDepts = await this.departmentService.getAllChildDeptIds(deptId); + const result = await db.staff.findMany({ + where: { + deptId: { in: [...childDepts, deptId] }, + domainId, + }, + }); + return result; + } + /** + * 创建新的员工记录 + * @param data 员工创建信息 + * @returns 新创建的员工记录 + */ + async create(data: z.infer) { + const { ...others } = data; + + try { + return await db.$transaction(async (transaction) => { + // 获取当前最大order值 + const maxOrder = await transaction.staff.aggregate({ + _max: { order: true }, + }); + // 新员工的order值比现有最大order值大1 + const newOrder = (maxOrder._max.order ?? -1) + 1; + // 创建新员工记录 + const newStaff = await transaction.staff.create({ + data: { ...others, order: newOrder }, + include: { domain: true, department: true }, + }); + return newStaff; + }); + } catch (error) { + console.error('Failed to create staff:', error); + throw error; + } + } + /** + * 更新员工记录 + * @param data 包含id和其他更新字段的对象 + * @returns 更新后的员工记录 + */ + async update(data: z.infer) { + const { id, ...others } = data; + try { + return await db.$transaction(async (transaction) => { + // 更新员工记录 + const updatedStaff = await transaction.staff.update({ + where: { id }, + data: others, + include: { domain: true, department: true }, + }); + return updatedStaff; + }); + } catch (error) { + console.error('Failed to update staff:', error); + throw error; + } + } + /** + * 批量删除员工记录(软删除) + * @param data 包含要删除的员工ID数组的对象 + * @returns 删除操作结果,包括删除的记录数 + */ + 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 deletedStaffs = await db.staff.updateMany({ + where: { id: { in: ids } }, + data: { deletedAt: new Date() }, + }); + if (!deletedStaffs.count) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: 'No taxonomies were found with the provided IDs.', + }); + } + return { success: true, count: deletedStaffs.count }; + } + /** + * 分页查询员工 + * @param data 包含分页参数、域ID和部门ID的对象 + * @returns 员工列表及总记录数 + */ + async paginate(data: z.infer) { + const { page, pageSize, domainId, deptId, ids } = data; + const childDepts = await this.departmentService.getAllChildDeptIds(deptId); + const [items, totalCount] = await Promise.all([ + db.staff.findMany({ + skip: (page - 1) * pageSize, + take: pageSize, + orderBy: { order: 'asc' }, + where: { + id: ids ? { in: ids } : undefined, + deletedAt: null, + domainId, + deptId: deptId ? { in: [...childDepts, deptId] } : undefined, + }, + include: { domain: true, department: true }, + }), + db.staff.count({ + where: { + deletedAt: null, + domainId, + deptId: deptId ? { in: [...childDepts, deptId] } : undefined, + }, + }), + ]); + const processedItems = await Promise.all( + items.map((item) => this.genStaffDto(item)), + ); + return { items: processedItems, totalCount }; + } + /** + * 根据关键词或ID集合查找员工 + * @param data 包含关键词、域ID和ID集合的对象 + * @returns 匹配的员工记录列表 + */ + async findMany(data: z.infer) { + const { keyword, domainId, ids } = data; + + return await db.staff.findMany({ + where: { + deletedAt: null, + domainId, + OR: [ + { username: { contains: keyword } }, + { + id: { in: ids }, + }, + ], + }, + orderBy: { order: 'asc' }, + take: 10, + }); + } + /** + * 生成员工的数据传输对象(DTO) + * @param staff 员工记录 + * @returns 含角色ID列表的员工DTO + */ + private async genStaffDto(staff: Staff) { + const roleMaps = await db.roleMap.findMany({ + where: { + domainId: staff.domainId, + objectId: staff.id, + objectType: ObjectType.STAFF, + }, + include: { role: true }, + }); + const roleIds = roleMaps.map((roleMap) => roleMap.role.id); + return { ...staff, roleIds }; + } + + + +} diff --git a/apps/server/src/tasks/tasks.module.ts b/apps/server/src/tasks/tasks.module.ts new file mode 100644 index 0000000..eb9717f --- /dev/null +++ b/apps/server/src/tasks/tasks.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { TasksService } from './tasks.service'; +import { InitModule } from '@server/init/init.module'; +@Module({ + imports: [InitModule], + providers: [TasksService] +}) +export class TasksModule { } diff --git a/apps/server/src/tasks/tasks.service.ts b/apps/server/src/tasks/tasks.service.ts new file mode 100644 index 0000000..dcb3dbb --- /dev/null +++ b/apps/server/src/tasks/tasks.service.ts @@ -0,0 +1,27 @@ +import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; +import { SchedulerRegistry } from '@nestjs/schedule'; +import { InitService } from '@server/init/init.service'; +import { CronJob } from 'cron'; + +@Injectable() +export class TasksService implements OnModuleInit { + private readonly logger = new Logger(TasksService.name); + + constructor( + private readonly schedulerRegistry: SchedulerRegistry, + private readonly initService: InitService, + ) { } + + async onModuleInit() { + this.logger.log('Main node launch'); + await this.initService.init(); + + const handleCronJob = new CronJob('0 * * * *', () => { + this.logger.log('cron job test'); + }); + + this.schedulerRegistry.addCronJob('cronJob', handleCronJob); + this.logger.log('Start cron job'); + handleCronJob.start(); + } +} diff --git a/apps/server/src/trpc/trpc.router.ts b/apps/server/src/trpc/trpc.router.ts index 7ecf6c0..bbc6944 100644 --- a/apps/server/src/trpc/trpc.router.ts +++ b/apps/server/src/trpc/trpc.router.ts @@ -1,21 +1,19 @@ import { INestApplication, Injectable } from '@nestjs/common'; +import { AuthRouter } from '@server/auth/auth.router'; import { TrpcService } from '@server/trpc/trpc.service'; import * as trpcExpress from '@trpc/server/adapters/express'; -import { HelloRouter } from '@server/hello/hello.router'; - @Injectable() export class TrpcRouter { - constructor(private readonly trpc: TrpcService, private readonly hello: HelloRouter) { } - + constructor(private readonly trpc: TrpcService, private readonly auth: AuthRouter) { } appRouter = this.trpc.router({ - hello: this.hello.router + auth: this.auth.router }); - async applyMiddleware(app: INestApplication) { app.use( `/trpc`, trpcExpress.createExpressMiddleware({ router: this.appRouter, + createContext: this.trpc.createContext }), ); } diff --git a/apps/server/src/trpc/trpc.service.ts b/apps/server/src/trpc/trpc.service.ts index 33d333b..117e582 100644 --- a/apps/server/src/trpc/trpc.service.ts +++ b/apps/server/src/trpc/trpc.service.ts @@ -1,13 +1,59 @@ import { Injectable } from '@nestjs/common'; -import { initTRPC } from '@trpc/server'; +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 { JwtService } from '@nestjs/jwt'; +type Context = Awaited>; @Injectable() export class TrpcService { - trpc = initTRPC.create({ - transformer: superjson + constructor(private readonly jwtService: JwtService) { } + async createContext({ + req, + res, + }: trpcExpress.CreateExpressContextOptions) { + const token = req.headers.authorization?.split(' ')[1]; + let tokenData: TokenPayload | 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; + if (tokenData) { + // Fetch staff details from the database using tokenData.id + staff = await db.staff.findUnique({ where: { id: tokenData.id } }); + if (!staff) { + throw new TRPCError({ code: 'UNAUTHORIZED', message: "User not found" }); + } + } + } catch (error) { + // Enhanced error handling for invalid session data or token verification failure + throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: "Invalid session data or token" }); + } + } + + return { + staff, + }; + }; + trpc = initTRPC.context().create({ + transformer: superjson, }); procedure = this.trpc.procedure; router = this.trpc.router; mergeRouters = this.trpc.mergeRouters; + // Define a protected procedure that ensures the user is authenticated + protectProcedure = this.procedure.use(async ({ ctx, next }) => { + if (!ctx.staff) { + throw new TRPCError({ code: 'UNAUTHORIZED', message: "Unauthorized request" }); + } + return next({ + ctx: { + // User value is confirmed to be non-null at this point + staff: ctx.staff, + }, + }); + }); } diff --git a/apps/server/tsconfig.json b/apps/server/tsconfig.json index 55da539..51b120f 100644 --- a/apps/server/tsconfig.json +++ b/apps/server/tsconfig.json @@ -10,6 +10,7 @@ "target": "ES2021", "sourceMap": true, "outDir": "./dist", + "strictNullChecks": false, // "baseUrl": "./", // "incremental": true, // "skipLibCheck": true, diff --git a/docker-compose.exmaple.yml b/docker-compose.exmaple.yml index 6a4fe71..37309de 100755 --- a/docker-compose.exmaple.yml +++ b/docker-compose.exmaple.yml @@ -20,7 +20,7 @@ services: - ./volumes/minio:/minio_data environment: - MINIO_ACCESS_KEY=minioadmin - - MINIO_SECRET_KEY=minioadmin + - MINIO_JWT_SECRET=minioadmin command: minio server /minio_data --console-address ":9001" -address ":9000" healthcheck: test: diff --git a/packages/common/prisma/schema.prisma b/packages/common/prisma/schema.prisma index e69de29..cb8d4df 100755 --- a/packages/common/prisma/schema.prisma +++ b/packages/common/prisma/schema.prisma @@ -0,0 +1,162 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model Taxonomy { + id String @id @default(uuid()) + name String @unique + deletedAt DateTime? + terms Term[] + order Int + + @@index([order, deletedAt]) +} + +model Relation { + id String @id @default(uuid()) + aId String + bId String + aType String + bType String + relationType String + createdAt DateTime? @default(now()) + + @@unique([aId, bId, aType, bType, relationType]) + @@map("relations") +} + +model Term { + id String @id @default(uuid()) + name String + taxonomy Taxonomy? @relation(fields: [taxonomyId], references: [id]) + taxonomyId String? + order Int + description String? + parentId String? + parent Term? @relation("ChildParent", fields: [parentId], references: [id], onDelete: Cascade) + children Term[] @relation("ChildParent") + ancestors TermAncestry[] @relation("DescendantToAncestor") + descendants TermAncestry[] @relation("AncestorToDescendant") + + domainId String? + domain Department? @relation("TermDom", fields: [domainId], references: [id]) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime? + createdBy String + createdStaff Staff? @relation(fields: [staffId], references: [id]) + staffId String? + + @@index([name]) // 对name字段建立索引,以加快基于name的查找速度 + @@index([parentId]) // 对parentId字段建立索引,以加快基于parentId的查找速度 + @@map("terms") +} + +model TermAncestry { + id String @id @default(uuid()) + ancestorId String + descendantId String + relDepth Int + ancestor Term @relation("AncestorToDescendant", fields: [ancestorId], references: [id], onDelete: Cascade) + descendant Term @relation("DescendantToAncestor", fields: [descendantId], references: [id], onDelete: Cascade) + createdAt DateTime? @default(now()) +} + +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]) + + @@map("comments") +} + +model Staff { + id String @id @default(uuid()) + username String @unique + password String + phoneNumber String? @unique + domainId String? + deptId String? + domain Department? @relation("DomainStaff", fields: [domainId], references: [id]) + department Department? @relation("DeptStaff", fields: [deptId], references: [id]) + registerToken String? + order Int + deletedAt DateTime? + system Boolean? @default(false) + comments Comment[] + terms Term[] + refreshTokens RefreshToken[] +} + +model RefreshToken { + id String @id @default(uuid()) + token String @unique + staffId String + staff Staff @relation(fields: [staffId], references: [id]) + createdAt DateTime @default(now()) + + @@map("refreshTokens") +} + +model Department { + id String @id @default(uuid()) + name String + order Int + ancestors DeptAncestry[] @relation("DescendantToAncestor") + descendants DeptAncestry[] @relation("AncestorToDescendant") + parentId String? @map("parentId") + parent Department? @relation("ChildParent", fields: [parentId], references: [id]) + children Department[] @relation("ChildParent") + domainTerms Term[] @relation("TermDom") + deletedAt DateTime? + isDomain Boolean? @default(false) + domainStaffs Staff[] @relation("DomainStaff") + deptStaffs Staff[] @relation("DeptStaff") +} + +model DeptAncestry { + ancestorId String + descendantId String + relDepth Int + ancestor Department @relation("AncestorToDescendant", fields: [ancestorId], references: [id]) + descendant Department @relation("DescendantToAncestor", fields: [descendantId], references: [id]) + + @@id([descendantId, ancestorId]) + @@index([ancestorId]) // 对ancestorId字段建立索引,以加快基于ancestorId的查找速度 + @@index([descendantId]) // 对descendantId字段建立索引,以加快基于descendantId的查找速度 +} + +model RoleMap { + id String @id @default(uuid()) + objectId String + roleId String + domainId String? + objectType String + role Role @relation(fields: [roleId], references: [id]) +} + +model Role { + id String @id @default(uuid()) + name String @unique + permissions String[] @default([]) + roleMaps RoleMap[] + deletedAt DateTime? + system Boolean? @default(false) +} diff --git a/packages/common/src/constants.ts b/packages/common/src/constants.ts new file mode 100644 index 0000000..6282219 --- /dev/null +++ b/packages/common/src/constants.ts @@ -0,0 +1,55 @@ +import { RolePerms } from "./enum"; + +export const InitRoles: { name: string, permissions: string[], system?: boolean }[] = [ + { + name: "基层", + permissions: [ + RolePerms.CREATE_TROUBLE, + RolePerms.CREATE_WORKPROGRESS, + ] + }, + { + name: "机关", + permissions: [ + RolePerms.CREATE_TROUBLE, + RolePerms.CREATE_WORKPROGRESS, + ], + }, + { + name: "领导", + permissions: [ + RolePerms.READ_DOM_TROUBLE, + RolePerms.CREATE_INSTRUCTION, + ], + }, + { + name: "域管理员", + permissions: Object.keys(RolePerms).filter( + (perm) => + ![ + RolePerms.READ_ANY_CHART, + RolePerms.READ_ANY_TROUBLE, + RolePerms.READ_ANY_TERM, + RolePerms.PROCESS_ANY_ASSESSMENT, + RolePerms.PROCESS_ANY_TROUBLE, + RolePerms.EDIT_ROOT_OPTION, + RolePerms.EDIT_ANY_TERM, + RolePerms.EDIT_ANY_TROUBLE, + RolePerms.EDIT_ANY_ASSESSMENT, + RolePerms.DELETE_ANY_TROUBLE, + RolePerms.DELETE_ANY_TERM, + RolePerms.DELETE_ANY_ASSESSMENT, + ].includes(perm as any) + ) as RolePerms[], + }, + { + name: "根管理员", + permissions: Object.keys(RolePerms) as RolePerms[], + }, +]; +export const InitTaxonomies: { name: string }[] = [{ + name: '分类' +}, +{ + name: '研判单元' +}] \ No newline at end of file diff --git a/packages/common/src/enum.ts b/packages/common/src/enum.ts new file mode 100644 index 0000000..67105c0 --- /dev/null +++ b/packages/common/src/enum.ts @@ -0,0 +1,57 @@ +export enum ObjectType { + DEPARTMENT = "DEPARTMENT", + STAFF = "STAFF", + COMMENT = "COMMENT", + TERM = "TERM", +} +export enum RelationType { + WATCH = "WATCH", + READED = "READED", + MESSAGE = "MESSAGE", +} +export enum RolePerms { + // Create Permissions 创建权限 + CREATE_ALERT = "CREATE_ALERT", // 创建警报 + CREATE_INSTRUCTION = "CREATE_INSTRUCTION", // 创建指令 + CREATE_TROUBLE = "CREATE_TROUBLE", // 创建问题 + CREATE_WORKPROGRESS = "CREATE_WORKPROGRESS", // 创建工作进度 + CREATE_ASSESSMENT = "CREATE_ASSESSMENT", // 创建评估 + CREATE_TERM = "CREATE_TERM", // 创建术语 + + // Read Permissions 读取权限 + READ_ANY_TROUBLE = "READ_ANY_TROUBLE", // 读取任何问题 + READ_DOM_TROUBLE = "READ_DOM_TROUBLE", // 读取领域问题 + READ_ANY_CHART = "READ_ANY_CHART", // 读取任何图表 + READ_DOM_CHART = "READ_DOM_CHART", // 读取领域图表 + READ_ANY_ASSESSMENT = "READ_ANY_ASSESSMENT", // 读取任何评估 + READ_DOM_ASSESSMENT = "READ_DOM_ASSESSMENT", // 读取领域评估 + READ_ANY_TERM = "READ_ANY_TERM", // 读取任何术语 + READ_DOM_TERM = "READ_DOM_TERM", // 读取领域术语 + + // Edit Permissions 编辑权限 + EDIT_DOM_TROUBLE = "EDIT_DOM_TROUBLE", // 编辑领域问题 + EDIT_ANY_TROUBLE = "EDIT_ANY_TROUBLE", // 编辑任何问题 + EDIT_DOM_ROLE = "EDIT_DOM_ROLE", // 编辑领域角色 + EDIT_ROOT_OPTION = "EDIT_ROOT_OPTION", // 编辑根选项 + EDIT_DOM_ASSESSMENT = "EDIT_DOM_ASSESSMENT", // 编辑领域评估 + EDIT_ANY_ASSESSMENT = "EDIT_ANY_ASSESSMENT", // 编辑任何评估 + EDIT_DOM_TERM = "EDIT_DOM_TERM", // 编辑领域术语 + EDIT_ANY_TERM = "EDIT_ANY_TERM", // 编辑任何术语 + + // Delete Permissions 删除权限 + DELETE_DOM_TROUBLE = "DELETE_DOM_TROUBLE", // 删除领域问题 + DELETE_ANY_TROUBLE = "DELETE_ANY_TROUBLE", // 删除任何问题 + DELETE_DOM_ASSESSMENT = "DELETE_DOM_ASSESSMENT", // 删除领域评估 + DELETE_ANY_ASSESSMENT = "DELETE_ANY_ASSESSMENT", // 删除任何评估 + DELETE_DOM_TERM = "DELETE_DOM_TERM", // 删除领域术语 + DELETE_ANY_TERM = "DELETE_ANY_TERM", // 删除任何术语 + + // Process Permissions 处理权限 + PROCESS_DOM_TROUBLE = "PROCESS_DOM_TROUBLE", // 处理领域问题 + PROCESS_ANY_TROUBLE = "PROCESS_ANY_TROUBLE", // 处理任何问题 + PROCESS_DOM_ASSESSMENT = "PROCESS_DOM_ASSESSMENT", // 处理领域评估 + PROCESS_ANY_ASSESSMENT = "PROCESS_ANY_ASSESSMENT", // 处理任何评估 + + // Audit Permissions 审核权限 + AUDIT_TROUBLE = "AUDIT_TROUBLE", // 审核问题 +} \ No newline at end of file diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index f86e89e..188949a 100755 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -1,4 +1,8 @@ +export * from "@prisma/client" +export * from "zod" export * from "./db" -export * from '@prisma/client'; - -export * from "./schema" \ No newline at end of file +export * from "./schema" +export * from "./enum" +export * from "./type" +export * from "./utils" +export * from "./constants" \ No newline at end of file diff --git a/packages/common/src/schema.ts b/packages/common/src/schema.ts index 3deda80..d1e48e6 100755 --- a/packages/common/src/schema.ts +++ b/packages/common/src/schema.ts @@ -1 +1,92 @@ -export { } \ No newline at end of file +import { z } from "zod" +export const AuthSchema = { + signInRequset: z.object({ + username: z.string(), + password: z.string(), + }), + signUpRequest: z.object({ + username: z.string(), + password: z.string(), + }), + changePassword: z.object({ + username: z.string(), + oldPassword: z.string(), + newPassword: z.string(), + }), + refreshTokenRequest: z.object({ + refreshToken: z.string(), + }), + logoutRequest: z.object({ + refreshToken: z.string(), + }), +}; +export const StaffSchema = { + create: z.object({ + username: z.string(), + password: z.string(), + domainId: z.string().nullish(), + }), + update: z.object({ + id: z.string(), + name: z.string().nullish(), + password: z.string().nullish(), + domainId: z.string().nullish(), + deptId: z.string().nullish(), + phoneNumber: z.string().nullish(), + order: z.number().nullish(), + registerToken: z.string().nullish(), + }), + delete: z.object({ + id: z.string(), + }), + batchDelete: z.object({ + ids: z.array(z.string()), + }), + findByDept: z.object({ + deptId: z.string(), + domainId: z.string().nullish(), + }), + findMany: z.object({ + keyword: z.string().nullish(), + domainId: z.string().nullish(), + ids: z.array(z.string()).nullish(), + }), + findUnique: z.object({ + + id: z.string().nullish(), + }), + paginate: z.object({ + page: z.number(), + pageSize: z.number(), + domainId: z.string().nullish(), + deptId: z.string().nullish(), + ids: z.array(z.string()).nullish(), + }), +}; +export const DepartmentSchema = { + create: z.object({ + name: z.string(), + parentId: z.string().nullish(), + isDomain: z.boolean().nullish(), + }), + update: z.object({ + id: z.string(), + name: z.string().nullish(), + parentId: z.string().nullish(), + deletedAt: z.date().nullish(), + order: z.number().nullish(), + isDomain: z.boolean().nullish(), + }), + delete: z.object({ + id: z.string(), + }), + findMany: z.object({ + keyword: z.string().nullish(), + ids: z.array(z.string()).nullish(), + }), + paginate: z.object({ + page: z.number(), + pageSize: z.number(), + ids: z.array(z.string()).nullish(), + }), +}; diff --git a/packages/common/src/type.ts b/packages/common/src/type.ts new file mode 100644 index 0000000..e8d68d5 --- /dev/null +++ b/packages/common/src/type.ts @@ -0,0 +1,19 @@ +import { Department, Staff } from "@prisma/client"; + +export interface DataNode { + title: any; + key: string; + hasChildren?: boolean; + children?: DataNode[]; + value: string; + data?: any; + isLeaf?: boolean; +} +export type StaffDto = Staff & { + domain?: Department; + department?: Department; +}; +export interface TokenPayload { + id: string; + username: string; +} \ No newline at end of file diff --git a/packages/common/src/utils.ts b/packages/common/src/utils.ts new file mode 100644 index 0000000..74ce84b --- /dev/null +++ b/packages/common/src/utils.ts @@ -0,0 +1,106 @@ +import { Staff } from "@prisma/client"; +import { DataNode } from "./type"; +export function findNodeByKey( + nodes: DataNode[], + targetKey: string +): DataNode | null { + let result: DataNode | null = null; + + for (const node of nodes) { + if (node.key === targetKey) { + return node; + } + + if (node.children && node.children.length > 0) { + result = findNodeByKey(node.children, targetKey); + if (result) { + return result; + } + } + } + + return result; +} +export function findStaffById( + nodes: DataNode[], + staffId: string +): Staff | null { + for (const node of nodes) { + // 在当前节点的staffs数组中查找 + const foundStaff = node?.data?.staffs.find( + (staff: Staff) => staff.id === staffId + ); + if (foundStaff) { + return foundStaff; + } + + // 如果当前节点的staffs数组中没有找到,则递归在子节点中查找 + if (node.children) { + const foundInChildren = findStaffById(node.children, staffId); + if (foundInChildren) { + return foundInChildren; + } + } + } + + // 如果在所有节点及其子节点中都没有找到,返回null + return null; +} +interface MappingConfig { + titleField?: string; + keyField?: string; + valueField?: string; + hasChildrenField?: string; // Optional, in case the structure has nested items + childrenField?: string; +} + +export function mapToDataNodes( + inputArray: any[], + config: MappingConfig = {} +): DataNode[] { + const { + titleField = "title", + keyField = "key", + valueField = "value", + hasChildrenField = "hasChildren", + childrenField = "children" + } = config; + + return inputArray.map((item) => { + const hasChildren = item[hasChildrenField] || false; + const children = item[childrenField] + return { + title: item[titleField] || "", + key: item[keyField] || "", + value: item[valueField] || null, + data: item, + children: children + ? mapToDataNodes(children, { titleField, keyField, valueField, hasChildrenField, childrenField }) + : undefined, + hasChildren + }; + }); +} + +/** + * 合并两个数组并去重。 + * + * 该函数将两个输入数组的元素合并为一个数组, + * 并确保结果数组中没有重复的元素。元素的顺序根据它们首次出现的顺序保留。 + * + * @template T - 输入数组中元素的类型。 + * @param {T[]} array1 - 要合并的第一个数组。 + * @param {T[]} array2 - 要合并的第二个数组。 + * @returns {T[]} 包含来自两个输入数组的唯一元素的新数组。 + * + * @example + * const array1 = [1, 2, 3, 4]; + * const array2 = [3, 4, 5, 6]; + * const result = mergeAndDeduplicate(array1, array2); + * console.log(result); // 输出: [1, 2, 3, 4, 5, 6] + */ +export function mergeAndDeduplicate(array1: T[], array2: T[]): T[] { + const set = new Set([...array1, ...array2]); + return Array.from(set); +} +