diff --git a/packages/cli/commands/src/version.ts b/packages/cli/commands/src/version.ts index 3122dfce..0365c6bf 100644 --- a/packages/cli/commands/src/version.ts +++ b/packages/cli/commands/src/version.ts @@ -24,7 +24,7 @@ export default async function () { if (toolkitName) { try { const pkgPath = path.join(home.getModulesPath(), toolkitName, 'package.json'); - log.debug(`${toolkitName} pacage.json path = ${pkgPath}`); + log.debug(`${toolkitName} package.json path = ${pkgPath}`); const pkg = require(pkgPath); console.log(chalk.magenta(`${toolkitName} v${pkg.version}`)); } catch (e) { diff --git a/packages/toolkits/pro/template/server/nestJs/.env b/packages/toolkits/pro/template/server/nestJs/.env new file mode 100644 index 00000000..f05f0dd9 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/.env @@ -0,0 +1,15 @@ +DATABASE_HOST = 'localhost' +DATABASE_PORT = 3306 +DATABASE_USERNAME = 'root' +DATABASE_PASSWORD = 'root' +DATABASE_NAME = 'ospp-nest' +DATABASE_SYNCHRONIZE = 'true' +DATABASE_AUTOLOADENTITIES = 'true' + +AUTH_SECRET = 'secret' + +REDIS_SECONDS = 7200 +REDIS_HOST = 'localhost' +REDIS_PORT = 6379 + +EXPIRES_IN = '2h' diff --git a/packages/toolkits/pro/template/server/nestJs/.gitignore b/packages/toolkits/pro/template/server/nestJs/.gitignore index b5e5f975..12a9d9ff 100644 --- a/packages/toolkits/pro/template/server/nestJs/.gitignore +++ b/packages/toolkits/pro/template/server/nestJs/.gitignore @@ -18,4 +18,7 @@ npm-debug.log /.nyc_output # dist -/dist \ No newline at end of file +/dist + +#env +/.env diff --git a/packages/toolkits/pro/template/server/nestJs/libs/db/src/db.module.ts b/packages/toolkits/pro/template/server/nestJs/libs/db/src/db.module.ts new file mode 100644 index 00000000..bd9c9d54 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/libs/db/src/db.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { DbService } from './db.service'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +@Module({ + imports: [ + TypeOrmModule.forRootAsync({ + useClass: DbService, + }), + ], + providers: [DbService], + exports: [DbService], +}) +export class DbModule {} diff --git a/packages/toolkits/pro/template/server/nestJs/libs/db/src/db.service.spec.ts b/packages/toolkits/pro/template/server/nestJs/libs/db/src/db.service.spec.ts new file mode 100644 index 00000000..d81007f4 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/libs/db/src/db.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { DbService } from './db.service'; + +describe('DbService', () => { + let service: DbService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [DbService], + }).compile(); + + service = module.get(DbService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/packages/toolkits/pro/template/server/nestJs/libs/db/src/db.service.ts b/packages/toolkits/pro/template/server/nestJs/libs/db/src/db.service.ts new file mode 100644 index 00000000..325c23fc --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/libs/db/src/db.service.ts @@ -0,0 +1,21 @@ +import { Injectable } from '@nestjs/common'; +import { TypeOrmModuleOptions, TypeOrmOptionsFactory } from '@nestjs/typeorm'; + +@Injectable() +export class DbService implements TypeOrmOptionsFactory { + // 注入config service取得env变量 + constructor() {} + // 回传TypeOrmOptions对象 + createTypeOrmOptions(): TypeOrmModuleOptions { + return { + type: 'mysql', + host: process.env.DATABASE_HOST, + port: parseInt(process.env.DATABASE_PORT), + username: process.env.DATABASE_USERNAME, + password: process.env.DATABASE_PASSWORD, + database: process.env.DATABASE_NAME, + synchronize: process.env.DATABASE_SYNCHRONIZE === 'true', + autoLoadEntities: process.env.DATABASE_AUTOLOADENTITIES === 'true', + }; + } +} diff --git a/packages/toolkits/pro/template/server/nestJs/libs/db/src/index.ts b/packages/toolkits/pro/template/server/nestJs/libs/db/src/index.ts new file mode 100644 index 00000000..07f92699 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/libs/db/src/index.ts @@ -0,0 +1,2 @@ +export * from './db.module'; +export * from './db.service'; diff --git a/packages/toolkits/pro/template/server/nestJs/libs/db/tsconfig.lib.json b/packages/toolkits/pro/template/server/nestJs/libs/db/tsconfig.lib.json new file mode 100644 index 00000000..dfa98ed6 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/libs/db/tsconfig.lib.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": true, + "outDir": "../../dist/libs/db" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] +} diff --git a/packages/toolkits/pro/template/server/nestJs/libs/models/src/index.ts b/packages/toolkits/pro/template/server/nestJs/libs/models/src/index.ts new file mode 100644 index 00000000..37cf49b2 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/libs/models/src/index.ts @@ -0,0 +1,4 @@ +export * from './user'; +export * from './permission'; +export * from './role'; +export * from './menu'; diff --git a/packages/toolkits/pro/template/server/nestJs/libs/models/src/menu.ts b/packages/toolkits/pro/template/server/nestJs/libs/models/src/menu.ts new file mode 100644 index 00000000..e543a55f --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/libs/models/src/menu.ts @@ -0,0 +1,21 @@ +import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; + +@Entity('menu') +export class Menu { + @PrimaryGeneratedColumn() + id: number; + @Column() + name: string; + @Column() + order: number; + @Column({ nullable: true }) + parentId: number; + @Column() + menuType: string; + @Column({ nullable: true }) + icon: string; + @Column() + component: string; + @Column() + path: string; +} diff --git a/packages/toolkits/pro/template/server/nestJs/libs/models/src/permission.ts b/packages/toolkits/pro/template/server/nestJs/libs/models/src/permission.ts new file mode 100644 index 00000000..546f3e1a --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/libs/models/src/permission.ts @@ -0,0 +1,11 @@ +import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; + +@Entity('permission') +export class Permission { + @PrimaryGeneratedColumn() + id: number; + @Column() + desc: string; + @Column() + name: string; +} diff --git a/packages/toolkits/pro/template/server/nestJs/libs/models/src/role.ts b/packages/toolkits/pro/template/server/nestJs/libs/models/src/role.ts new file mode 100644 index 00000000..af361296 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/libs/models/src/role.ts @@ -0,0 +1,26 @@ +import { + Column, + Entity, + JoinTable, + ManyToMany, + PrimaryGeneratedColumn, +} from 'typeorm'; +import { Permission } from './permission'; +import { Menu } from './menu'; + +@Entity('role') +export class Role { + @PrimaryGeneratedColumn() + id: number; + @Column() + name: string; + @ManyToMany(() => Permission, { + onUpdate: 'CASCADE', + }) + @JoinTable({ name: 'role_permission' }) + permission: Permission[]; + + @ManyToMany(() => Menu) + @JoinTable({ name: 'role_menu' }) + menus: Menu[]; +} diff --git a/packages/toolkits/pro/template/server/nestJs/libs/models/src/user.ts b/packages/toolkits/pro/template/server/nestJs/libs/models/src/user.ts new file mode 100644 index 00000000..e7e0c235 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/libs/models/src/user.ts @@ -0,0 +1,49 @@ +import { + BeforeInsert, + Column, + CreateDateColumn, + DeleteDateColumn, + Entity, + JoinTable, + ManyToMany, + PrimaryColumn, + PrimaryGeneratedColumn, + UpdateDateColumn, +} from 'typeorm'; +import { Role } from './role'; +import * as crypto from 'crypto'; + +export const encry = (value: string, salt: string) => + crypto.pbkdf2Sync(value, salt, 1000, 18, 'sha256').toString('hex'); + +@Entity('user') +export class User { + @PrimaryGeneratedColumn() + id: number; + @Column() + name: string; + @Column() + email: string; + @Column() + password: string; + @ManyToMany(() => Role) + @JoinTable({ name: 'user_role' }) + role: Role[]; + @CreateDateColumn() + createTime: Date; + @UpdateDateColumn() + updateTime: Date; + @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' }) + create_time: Date; + @Column({ nullable: true }) + salt: string; + @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' }) + update_time: Date; + @Column({ type: 'bigint', nullable: true }) + deleteAt: number; + @BeforeInsert() + beforeInsert() { + this.salt = crypto.randomBytes(4).toString('base64'); + this.password = encry(this.password, this.salt); + } +} diff --git a/packages/toolkits/pro/template/server/nestJs/libs/models/tsconfig.lib.json b/packages/toolkits/pro/template/server/nestJs/libs/models/tsconfig.lib.json new file mode 100644 index 00000000..4777fe3b --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/libs/models/tsconfig.lib.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": true, + "outDir": "../../dist/libs/models" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] +} diff --git a/packages/toolkits/pro/template/server/nestJs/libs/redis/redis.module.ts b/packages/toolkits/pro/template/server/nestJs/libs/redis/redis.module.ts new file mode 100644 index 00000000..b4958682 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/libs/redis/redis.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { RedisService } from './redis.service'; + +@Module({ + providers: [RedisService], + exports: [RedisService], +}) +export class RedisModule {} diff --git a/packages/toolkits/pro/template/server/nestJs/libs/redis/redis.service.spec.ts b/packages/toolkits/pro/template/server/nestJs/libs/redis/redis.service.spec.ts new file mode 100644 index 00000000..9300ac3e --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/libs/redis/redis.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { RedisService } from './redis.service'; + +describe('RedisService', () => { + let service: RedisService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [RedisService], + }).compile(); + + service = module.get(RedisService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/packages/toolkits/pro/template/server/nestJs/libs/redis/redis.service.ts b/packages/toolkits/pro/template/server/nestJs/libs/redis/redis.service.ts new file mode 100644 index 00000000..b1b8c0cf --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/libs/redis/redis.service.ts @@ -0,0 +1,30 @@ +import { Injectable } from '@nestjs/common'; +import Redis from 'ioredis'; + +@Injectable() +export class RedisService { + private redisClient: Redis; + constructor() { + this.redisClient = new Redis({ + host: process.env.REDIS_HOST, + port: parseInt(process.env.REDIS_PORT), + }); + } + async setUserToken( + email: string, + token: string, + ttl: number + ): Promise { + return this.redisClient.set(`user:${email}:token`, token, 'EX', ttl); + } + + async getUserToken(email: string): Promise { + return this.redisClient.get(`user:${email}:token`); + } + + async delUserToken(email: string): Promise { + //退出登录后,将token从Redis删除 + await this.redisClient.del(`user:${email}:token`); + return; + } +} diff --git a/packages/toolkits/pro/template/server/nestJs/nest-cli.json b/packages/toolkits/pro/template/server/nestJs/nest-cli.json new file mode 100644 index 00000000..79fb856f --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/nest-cli.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json.schemastore.org/nest-cli", + "collection": "@nestjs/schematics", + "sourceRoot": "src", + "compilerOptions": { + "deleteOutDir": true, + "webpack": true + }, + "projects": { + "db": { + "type": "library", + "root": "libs/db", + "entryFile": "index", + "sourceRoot": "libs/db/src", + "compilerOptions": { + "tsConfigPath": "libs/db/tsconfig.lib.json" + } + }, + "models": { + "type": "library", + "root": "libs/models", + "entryFile": "index", + "sourceRoot": "libs/models/src", + "compilerOptions": { + "tsConfigPath": "libs/models/tsconfig.lib.json" + } + } + } +} \ No newline at end of file diff --git a/packages/toolkits/pro/template/server/nestJs/package.json b/packages/toolkits/pro/template/server/nestJs/package.json index 11f79817..e79d5e6b 100644 --- a/packages/toolkits/pro/template/server/nestJs/package.json +++ b/packages/toolkits/pro/template/server/nestJs/package.json @@ -20,15 +20,26 @@ }, "dependencies": { "@nestjs/common": "10.0.3", + "@nestjs/config": "^3.2.3", "@nestjs/core": "10.0.3", + "@nestjs/jwt": "^10.2.0", + "@nestjs/mapped-types": "*", + "@nestjs/microservices": "^10.3.10", "@nestjs/platform-express": "10.0.3", "@nestjs/sequelize": "10.0.0", + "@nestjs/typeorm": "^10.0.2", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.1", + "dotenv": "^16.4.5", + "ioredis": "^5.4.1", "mysql2": "3.4.3", + "redis": "^4.6.15", "reflect-metadata": "0.1.13", "rimraf": "5.0.1", "rxjs": "7.8.1", "sequelize": "6.32.1", "sequelize-typescript": "2.1.5", + "typeorm": "^0.3.20", "typescript": "5.1.6" }, "devDependencies": { @@ -46,6 +57,7 @@ "eslint-plugin-import": "2.27.5", "jest": "29.6.1", "prettier": "2.8.8", + "source-map-support": "^0.5.20", "supertest": "6.3.3", "ts-jest": "29.1.1", "ts-loader": "9.4.4", diff --git a/packages/toolkits/pro/template/server/nestJs/src/app.module.ts b/packages/toolkits/pro/template/server/nestJs/src/app.module.ts index 3d252121..ffe940c1 100644 --- a/packages/toolkits/pro/template/server/nestJs/src/app.module.ts +++ b/packages/toolkits/pro/template/server/nestJs/src/app.module.ts @@ -1,20 +1,124 @@ -import { Module } from '@nestjs/common'; import { SequelizeModule } from '@nestjs/sequelize'; -import { EmployeesModule } from './employees/employees.module'; +import { HttpException, Logger, Module, OnModuleInit } from '@nestjs/common'; +import { UserModule } from './user/user.module'; +import { DbModule } from '@app/db'; +import { PermissionModule } from './permission/permission.module'; +import { AuthModule } from './auth/auth.module'; +import { APP_GUARD } from '@nestjs/core'; +import { AuthGuard } from './auth/auth.guard'; +import { PermissionGuard } from './permission/permission.guard'; +import { RoleModule } from './role/role.module'; +import { join } from 'path'; +import { existsSync, writeFileSync } from 'fs'; +import { UserService } from './user/user.service'; +import { RoleService } from './role/role.service'; +import { PermissionService } from './permission/permission.service'; +import { Permission } from '@app/models'; +import { MenuModule } from './menu/menu.module'; +import { ConfigModule, ConfigService } from '@nestjs/config'; @Module({ imports: [ - SequelizeModule.forRoot({ - dialect: '', - host: '', - port: '', - username: '', - password: '', - database: '', - autoLoadModels: true, - synchronize: true, + DbModule, + UserModule, + PermissionModule, + AuthModule, + RoleModule, + MenuModule, + ConfigModule.forRoot({ + isGlobal: true, }), - EmployeesModule, + ], + providers: [ + { + provide: APP_GUARD, + useClass: AuthGuard, + }, + { + provide: APP_GUARD, + useClass: PermissionGuard, + }, ], }) -export class AppModule {} +export class AppModule implements OnModuleInit { + constructor( + private user: UserService, + private role: RoleService, + private permission: PermissionService + ) {} + async onModuleInit() { + const ROOT = __dirname; + const LOCK_FILE = join(ROOT, 'lock'); + if (existsSync(LOCK_FILE)) { + return; + } + // TODO: menu + const modules = ['user', 'permission', 'role', 'menu']; + const actions = ['add', 'remove', 'update', 'query']; + const tasks = []; + let permission; + let isInit = true; + try { + permission = await this.permission.create( + { + name: '*', + desc: 'super permission', + }, + isInit + ); + } catch (e) { + const err = e as HttpException; + Logger.error(err.message); + Logger.error(`Please clear the database and try again`); + process.exit(-1); + } + for (const module of modules) { + for (const action of actions) { + tasks.push( + this.permission.create( + { + name: `${module}::${action}`, + desc: '', + }, + isInit + ) + ); + } + } + const status = Promise.allSettled(tasks); + const statusData = await status; + const hasFail = statusData.some((data) => data.status === 'rejected'); + if (hasFail) { + const fail: any[] = statusData.filter( + (data) => data.status === 'rejected' + ); + fail.forEach((data) => { + Logger.error(`${data.reason}`); + }); + Logger.error('Please clear the database and try again'); + process.exit(-1); + } + const role = await this.role.create( + { + name: 'admin', + permissionIds: [permission.id], + menuIds: [], + }, + isInit + ); + const user = await this.user.create( + { + email: 'admin@no-reply.com', + password: 'admin', + roleIds: [role.id], + username: 'admin', + }, + isInit + ); + Logger.log(`[APP]: create admin user success`); + Logger.log(`[APP]: email: ${user.email}`); + Logger.log(`[APP]: password: 'admin'`); + Logger.log('Enjoy!'); + writeFileSync(LOCK_FILE, ''); + } +} diff --git a/packages/toolkits/pro/template/server/nestJs/src/auth/auth.controller.ts b/packages/toolkits/pro/template/server/nestJs/src/auth/auth.controller.ts new file mode 100644 index 00000000..db59250d --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/auth/auth.controller.ts @@ -0,0 +1,24 @@ +import { Body, Controller, Get, Post, UseGuards } from '@nestjs/common'; +import { AuthService } from './auth.service'; +import { CreateAuthDto } from './dto/create-auth.dto'; +import { LogoutAuthDto } from './dto/logout-auth.dto'; +import { Public } from '../public/public.decorator'; +import { Permission } from '../public/permission.decorator'; +import { AuthGuard } from './auth.guard'; + +@Controller('auth') +export class AuthController { + constructor(private readonly authService: AuthService) {} + + @Public() + @Post('login') + @UseGuards(AuthGuard) + async login(@Body() body: CreateAuthDto) { + return this.authService.login(body); + } + @Post('logout') + @UseGuards(AuthGuard) + async logout(@Body() body: LogoutAuthDto) { + return this.authService.logout(body.email); + } +} diff --git a/packages/toolkits/pro/template/server/nestJs/src/auth/auth.guard.ts b/packages/toolkits/pro/template/server/nestJs/src/auth/auth.guard.ts new file mode 100644 index 00000000..9b32b061 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/auth/auth.guard.ts @@ -0,0 +1,51 @@ +import { + CanActivate, + ExecutionContext, + HttpException, + HttpStatus, + Injectable, +} from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { JwtService } from '@nestjs/jwt'; +import { Request } from 'express'; +import { AuthService } from './auth.service'; + +@Injectable() +export class AuthGuard implements CanActivate { + constructor( + private readonly jwt: JwtService, + private readonly reflector: Reflector, + private readonly authService: AuthService + ) {} + async canActivate(ctx: ExecutionContext): Promise { + const isPublic = this.reflector.getAllAndOverride('isPublic', [ + ctx.getHandler(), + ctx.getClass(), + ]); + if (isPublic) { + return true; + } + const req = ctx.switchToHttp().getRequest(); + const token = this.extractTokenFromHeader(req); + if (!token) { + throw new HttpException('异常Token', HttpStatus.FORBIDDEN); + } + try { + const payload = await this.jwt.decode(token); + req['user'] = payload; + return this.authService.getToken(payload.email).then((redisToken) => { + // 如果Redis中没有token或者token不匹配,返回false + if (!redisToken || redisToken !== token) { + return false; + } + return true; + }); + } catch (err) { + throw new HttpException('登录过期', HttpStatus.BAD_REQUEST); + } + } + private extractTokenFromHeader(request: Request): string | undefined { + const [type, token] = request.headers.authorization?.split(' ') ?? []; + return type === 'Bearer' ? token : undefined; + } +} diff --git a/packages/toolkits/pro/template/server/nestJs/src/auth/auth.module.ts b/packages/toolkits/pro/template/server/nestJs/src/auth/auth.module.ts new file mode 100644 index 00000000..16b81c31 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/auth/auth.module.ts @@ -0,0 +1,31 @@ +import { Module } from '@nestjs/common'; +import { AuthService } from './auth.service'; +import { AuthController } from './auth.controller'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { User } from '@app/models'; +import { JwtModule } from '@nestjs/jwt'; +import { UserModule } from '../user/user.module'; +import { RedisService } from '../../libs/redis/redis.service'; +import { RedisModule } from '../../libs/redis/redis.module'; + +@Module({ + controllers: [AuthController], + providers: [AuthService, RedisService], + exports: [AuthService], + imports: [ + TypeOrmModule.forFeature([User]), + JwtModule.registerAsync({ + imports: [RedisModule], + useFactory: async () => ({ + secret: process.env.AUTH_SECRET, + global: true, + signOptions: { + expiresIn: process.env.EXPIRES_IN, + }, + }), + global: true, + }), + UserModule, + ], +}) +export class AuthModule {} diff --git a/packages/toolkits/pro/template/server/nestJs/src/auth/auth.service.ts b/packages/toolkits/pro/template/server/nestJs/src/auth/auth.service.ts new file mode 100644 index 00000000..f9c7d87b --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/auth/auth.service.ts @@ -0,0 +1,49 @@ +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { CreateAuthDto } from './dto/create-auth.dto'; +import { InjectRepository } from '@nestjs/typeorm'; +import { encry, User } from '@app/models'; +import { Repository } from 'typeorm'; +import { JwtService } from '@nestjs/jwt'; +import { RedisService } from '../../libs/redis/redis.service'; + +@Injectable() +export class AuthService { + constructor( + @InjectRepository(User) + private user: Repository, + private jwtService: JwtService, + private readonly redisService: RedisService + ) {} + + async getToken(userId: string): Promise { + return this.redisService.getUserToken(`user:${userId}:token`); + } + + async logout(email: string): Promise { + //退出登录后,将token从Redis删除 + await this.redisService.delUserToken(`user:${email}:token`); + return; + } + + async login(dto: CreateAuthDto) { + const { email, password } = dto; + const userInfo = await this.user.findOne({ where: { email } }); + if (userInfo === null || userInfo.deleteAt !== null) { + throw new HttpException('该用户不存在', HttpStatus.NOT_FOUND); + } + if (encry(password, userInfo.salt) !== userInfo.password) { + throw new HttpException('密码或邮箱错误', HttpStatus.BAD_REQUEST); + } + const payload = { + email, + }; + const token = this.jwtService.signAsync(payload); + //将token设置到Redis中,有效期2h + await this.redisService.setUserToken( + `user:${email}:token`, + await token, + await parseInt(process.env.REDIS_SECONDS) + ); + return token; + } +} diff --git a/packages/toolkits/pro/template/server/nestJs/src/auth/dto/create-auth.dto.ts b/packages/toolkits/pro/template/server/nestJs/src/auth/dto/create-auth.dto.ts new file mode 100644 index 00000000..7960875e --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/auth/dto/create-auth.dto.ts @@ -0,0 +1,8 @@ +import { IsNotEmpty } from 'class-validator'; + +export class CreateAuthDto { + @IsNotEmpty() + email: string; + @IsNotEmpty() + password: string; +} diff --git a/packages/toolkits/pro/template/server/nestJs/src/auth/dto/logout-auth.dto.ts b/packages/toolkits/pro/template/server/nestJs/src/auth/dto/logout-auth.dto.ts new file mode 100644 index 00000000..3c009c78 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/auth/dto/logout-auth.dto.ts @@ -0,0 +1,6 @@ +import { IsNotEmpty } from 'class-validator'; + +export class LogoutAuthDto { + @IsNotEmpty() + email: string; +} diff --git a/packages/toolkits/pro/template/server/nestJs/src/auth/dto/update-auth.dto.ts b/packages/toolkits/pro/template/server/nestJs/src/auth/dto/update-auth.dto.ts new file mode 100644 index 00000000..100de4fb --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/auth/dto/update-auth.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/mapped-types'; +import { CreateAuthDto } from './create-auth.dto'; + +export class UpdateAuthDto extends PartialType(CreateAuthDto) {} diff --git a/packages/toolkits/pro/template/server/nestJs/src/main.ts b/packages/toolkits/pro/template/server/nestJs/src/main.ts index e1e10d96..9eeada52 100644 --- a/packages/toolkits/pro/template/server/nestJs/src/main.ts +++ b/packages/toolkits/pro/template/server/nestJs/src/main.ts @@ -1,8 +1,13 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; +import { ValidationPipe } from '@nestjs/common'; +import * as dotenv from 'dotenv'; + +dotenv.config({ path: '.env' }); async function bootstrap() { const app = await NestFactory.create(AppModule); + app.useGlobalPipes(new ValidationPipe()); await app.listen(3000); console.log(`Application is running on: ${await app.getUrl()}`); } diff --git a/packages/toolkits/pro/template/server/nestJs/src/menu/__tests__/map2Tree.spec.ts b/packages/toolkits/pro/template/server/nestJs/src/menu/__tests__/map2Tree.spec.ts new file mode 100644 index 00000000..0008e9cb --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/menu/__tests__/map2Tree.spec.ts @@ -0,0 +1,207 @@ +import { convertToTree } from '../menu.service'; + +const cretaeMenuItem = ( + id: number, + name: string, + parentId: number | null, + order = 0 +) => { + return { + id, + name, + parentId, + order, + menuType: '', + icon: '', + component: '', + path: '', + }; +}; + +describe('convertToTree', () => { + it('mutil root', () => { + const menus = [ + cretaeMenuItem(1, '0', null, 0), + cretaeMenuItem(2, '1', null, 1), + cretaeMenuItem(3, '2', null, 2), + cretaeMenuItem(4, '3', null, 3), + cretaeMenuItem(5, '4', null, 5), + + cretaeMenuItem(6, '1-1', 1, 6), + cretaeMenuItem(7, '1-2', 2, 7), + cretaeMenuItem(8, '1-3', 1, 8), + cretaeMenuItem(9, '1-4', 1, 9), + cretaeMenuItem(10, '1-5', 1, 10), + + cretaeMenuItem(11, '2-1', 2, 11), + cretaeMenuItem(12, '2-2', 2, 12), + cretaeMenuItem(13, '2-3', 2, 13), + cretaeMenuItem(14, '2-4', 2, 14), + cretaeMenuItem(15, '2-5', 2, 15), + + cretaeMenuItem(16, '1-1-1', 6, 16), + cretaeMenuItem(17, '1-1-2', 6, 17), + cretaeMenuItem(18, '1-1-3', 6, 18), + cretaeMenuItem(19, '1-1-4', 6, 19), + cretaeMenuItem(20, '1-1-5', 6, 20), + ]; + const data = convertToTree(menus); + expect(data).toStrictEqual([ + { + label: '0', + id: 1, + children: [ + { + label: '1-1', + id: 6, + children: [ + { + label: '1-1-1', + id: 16, + children: [], + url: '', + }, + { + label: '1-1-2', + id: 17, + children: [], + url: '', + }, + { + label: '1-1-3', + id: 18, + children: [], + url: '', + }, + { + label: '1-1-4', + id: 19, + children: [], + url: '', + }, + { + label: '1-1-5', + id: 20, + children: [], + url: '', + }, + ], + url: '', + }, + { + label: '1-3', + id: 8, + children: [], + url: '', + }, + { + label: '1-4', + id: 9, + children: [], + url: '', + }, + { + label: '1-5', + id: 10, + children: [], + url: '', + }, + ], + url: '', + }, + { + label: '1', + id: 2, + children: [ + { + label: '1-2', + id: 7, + children: [], + url: '', + }, + { + label: '2-1', + id: 11, + children: [], + url: '', + }, + { + label: '2-2', + id: 12, + children: [], + url: '', + }, + { + label: '2-3', + id: 13, + children: [], + url: '', + }, + { + label: '2-4', + id: 14, + children: [], + url: '', + }, + { + label: '2-5', + id: 15, + children: [], + url: '', + }, + ], + url: '', + }, + { + label: '2', + id: 3, + children: [], + url: '', + }, + { + label: '3', + id: 4, + children: [], + url: '', + }, + { + label: '4', + id: 5, + children: [], + url: '', + }, + ]); + }); + it('empty', () => { + expect(convertToTree([])).toStrictEqual([]); + }); + it('not root', () => { + const menus = [ + cretaeMenuItem(1, '0', 0, 0), + cretaeMenuItem(2, '1', 0, 1), + cretaeMenuItem(3, '2', 0, 2), + cretaeMenuItem(4, '3', 0, 3), + cretaeMenuItem(5, '4', 0, 5), + + cretaeMenuItem(6, '1-1', 1, 6), + cretaeMenuItem(7, '1-2', 2, 7), + cretaeMenuItem(8, '1-3', 1, 8), + cretaeMenuItem(9, '1-4', 1, 9), + cretaeMenuItem(10, '1-5', 1, 10), + + cretaeMenuItem(11, '2-1', 2, 11), + cretaeMenuItem(12, '2-2', 2, 12), + cretaeMenuItem(13, '2-3', 2, 13), + cretaeMenuItem(14, '2-4', 2, 14), + cretaeMenuItem(15, '2-5', 2, 15), + + cretaeMenuItem(16, '1-1-1', 6, 16), + cretaeMenuItem(17, '1-1-2', 6, 17), + cretaeMenuItem(18, '1-1-3', 6, 18), + cretaeMenuItem(19, '1-1-4', 6, 19), + cretaeMenuItem(20, '1-1-5', 6, 20), + ]; + const data = convertToTree(menus); + console.log(data); + }); +}); diff --git a/packages/toolkits/pro/template/server/nestJs/src/menu/dto/create-menu.dto.ts b/packages/toolkits/pro/template/server/nestJs/src/menu/dto/create-menu.dto.ts new file mode 100644 index 00000000..80426070 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/menu/dto/create-menu.dto.ts @@ -0,0 +1,18 @@ +import { IsNotEmpty } from 'class-validator'; + +export class CreateMenuDto { + @IsNotEmpty() + order: number; + @IsNotEmpty() + menuType: string; + @IsNotEmpty() + name: string; + @IsNotEmpty() + path: string; + @IsNotEmpty() + component: string; + @IsNotEmpty() + icon: string; + + parentId: number | null; +} diff --git a/packages/toolkits/pro/template/server/nestJs/src/menu/dto/delete-menu.dto.ts b/packages/toolkits/pro/template/server/nestJs/src/menu/dto/delete-menu.dto.ts new file mode 100644 index 00000000..fba71a0f --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/menu/dto/delete-menu.dto.ts @@ -0,0 +1,8 @@ +import { IsNotEmpty } from 'class-validator'; + +export class DeleteMenuDto { + @IsNotEmpty() + id: number; + @IsNotEmpty() + name: string; +} diff --git a/packages/toolkits/pro/template/server/nestJs/src/menu/dto/update-menu.dto.ts b/packages/toolkits/pro/template/server/nestJs/src/menu/dto/update-menu.dto.ts new file mode 100644 index 00000000..a8de5514 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/menu/dto/update-menu.dto.ts @@ -0,0 +1,6 @@ +import { PartialType } from '@nestjs/mapped-types'; +import { CreateMenuDto } from './create-menu.dto'; + +export class UpdateMenuDto extends PartialType(CreateMenuDto) { + id: number; +} diff --git a/packages/toolkits/pro/template/server/nestJs/src/menu/menu.controller.ts b/packages/toolkits/pro/template/server/nestJs/src/menu/menu.controller.ts new file mode 100644 index 00000000..5913c059 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/menu/menu.controller.ts @@ -0,0 +1,42 @@ +import { + Controller, + Get, + Post, + Body, + Patch, + Req, + Delete, +} from '@nestjs/common'; +import { MenuService } from './menu.service'; +import { CreateMenuDto } from './dto/create-menu.dto'; +import { Permission } from '../public/permission.decorator'; +import { UpdateMenuDto } from './dto/update-menu.dto'; +import { DeleteMenuDto } from './dto/delete-menu.dto'; + +@Controller('menu') +export class MenuController { + constructor(private readonly menuService: MenuService) {} + + @Get() + async getMenus(@Req() req) { + return this.menuService.findAll(req.user); + } + + @Post() + @Permission('menu::add') + async createMenu(@Body() dto: CreateMenuDto) { + return this.menuService.createMenu(dto); + } + + @Patch() + @Permission('menu::update') + async updateMenu(@Body() dto: UpdateMenuDto) { + return this.menuService.updateMenu(dto); + } + + @Delete() + @Permission('menu::remove') + async deleteMenu(@Body() dto: DeleteMenuDto) { + return this.menuService.deleteMenu(dto); + } +} diff --git a/packages/toolkits/pro/template/server/nestJs/src/menu/menu.module.ts b/packages/toolkits/pro/template/server/nestJs/src/menu/menu.module.ts new file mode 100644 index 00000000..977ee110 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/menu/menu.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { MenuService } from './menu.service'; +import { MenuController } from './menu.controller'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Menu, Role, User } from '@app/models'; + +@Module({ + imports: [TypeOrmModule.forFeature([Menu, User, Role])], + controllers: [MenuController], + providers: [MenuService], +}) +export class MenuModule {} diff --git a/packages/toolkits/pro/template/server/nestJs/src/menu/menu.service.ts b/packages/toolkits/pro/template/server/nestJs/src/menu/menu.service.ts new file mode 100644 index 00000000..49e84d6b --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/menu/menu.service.ts @@ -0,0 +1,116 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Menu, User } from '@app/models'; +import { Repository } from 'typeorm'; +import { CreateMenuDto } from './dto/create-menu.dto'; +import { UpdateMenuDto } from './dto/update-menu.dto'; +import { DeleteMenuDto } from './dto/delete-menu.dto'; + +export interface ITreeNodeData { + // node-key='id' 设置节点的唯一标识 + id: number | string; + // 节点显示文本 + label: string; + // 子节点 + children?: ITreeNodeData[]; + // 链接 + url?: string; +} + +interface MenuMap { + [key: number]: Menu; +} + +const toNode = (menu: Menu): ITreeNodeData => { + return { + label: menu.name, + id: menu.id, + children: [], + url: menu.path, + }; +}; + +export const convertToTree = ( + menus: Menu[], + parentId: number | null = null +) => { + const tree: ITreeNodeData[] = []; + for (let i = 0; i < menus.length; i++) { + const menu = menus[i]; + if (menu.parentId === parentId) { + const children = convertToTree(menus, menu.id); + const node = toNode(menu); + node.children = children; + tree.push(node); + } + } + return tree; +}; + +@Injectable() +export class MenuService { + constructor( + @InjectRepository(User) + private user: Repository, + @InjectRepository(Menu) + private menu: Repository + ) {} + async findAll(user: User) { + const userInfo = await this.user + .createQueryBuilder('user') + .leftJoinAndSelect('user.role', 'role') + .leftJoinAndSelect('role.menus', 'menus') + .where({ + email: user.email, + }) + .orderBy('menus.order', 'ASC') + .getOne(); + const menus = userInfo.role.flatMap((role) => role.menus); + const maps: MenuMap = {}; + menus.forEach((menu) => { + maps[menu.id] = menu; + }); + return convertToTree(menus); + } + async createMenu(dto: CreateMenuDto) { + const { + order, + menuType, + name, + path, + component, + icon, + parentId = null, + } = dto; + return this.menu.save({ + name, + path, + component, + parentId, + menuType, + icon, + order, + }); + } + async updateMenu(newData: UpdateMenuDto) { + await this.menu.update(newData.id, { + name: newData.name, + path: newData.path, + component: newData.component, + parentId: newData.parentId, + menuType: newData.menuType, + icon: newData.icon, + order: newData.order, + }); + return true; + } + async deleteMenu(dto: DeleteMenuDto) { + const menu = this.menu.findOne({ + where: { + id: dto.id, + name: dto.name, + }, + }); + return this.menu.remove(await menu); + } +} diff --git a/packages/toolkits/pro/template/server/nestJs/src/permission/dto/create-permission.dto.ts b/packages/toolkits/pro/template/server/nestJs/src/permission/dto/create-permission.dto.ts new file mode 100644 index 00000000..5c6c16d7 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/permission/dto/create-permission.dto.ts @@ -0,0 +1,8 @@ +import { IsNotEmpty } from 'class-validator'; + +export class CreatePermissionDto { + @IsNotEmpty() + name: string; + @IsNotEmpty() + desc: string; +} diff --git a/packages/toolkits/pro/template/server/nestJs/src/permission/dto/delete-permission.dto.ts b/packages/toolkits/pro/template/server/nestJs/src/permission/dto/delete-permission.dto.ts new file mode 100644 index 00000000..58e93846 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/permission/dto/delete-permission.dto.ts @@ -0,0 +1,6 @@ +import { IsNotEmpty } from 'class-validator'; + +export class DeletePermissionDto { + @IsNotEmpty() + name: string; +} diff --git a/packages/toolkits/pro/template/server/nestJs/src/permission/dto/update-permission.dto.ts b/packages/toolkits/pro/template/server/nestJs/src/permission/dto/update-permission.dto.ts new file mode 100644 index 00000000..739129d0 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/permission/dto/update-permission.dto.ts @@ -0,0 +1,8 @@ +import { PartialType } from '@nestjs/mapped-types'; +import { CreatePermissionDto } from './create-permission.dto'; +import { IsNotEmpty } from 'class-validator'; + +export class UpdatePermissionDto extends PartialType(CreatePermissionDto) { + @IsNotEmpty() + id: number; +} diff --git a/packages/toolkits/pro/template/server/nestJs/src/permission/permission.controller.ts b/packages/toolkits/pro/template/server/nestJs/src/permission/permission.controller.ts new file mode 100644 index 00000000..35eb9816 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/permission/permission.controller.ts @@ -0,0 +1,35 @@ +import { Body, Controller, Delete, Get, Patch, Post } from '@nestjs/common'; +import { PermissionService } from './permission.service'; +import { CreatePermissionDto } from './dto/create-permission.dto'; +import { DeletePermissionDto } from './dto/delete-permission.dto'; +import { Permission } from '../public/permission.decorator'; +import { UpdatePermissionDto } from './dto/update-permission.dto'; + +@Controller('permission') +export class PermissionController { + constructor(private readonly permissionService: PermissionService) {} + + @Permission('permission::create') + @Post() + create(@Body() dto: CreatePermissionDto) { + return this.permissionService.create(dto, false); + } + + @Patch() + @Permission('permission::update') + update(@Body() dto: UpdatePermissionDto) { + return this.permissionService.updatePermission(dto); + } + + @Get() + @Permission('permission::get') + find() { + return this.permissionService.findPermission(); + } + + @Delete() + @Permission('permission::remove') + del(@Body() dto: DeletePermissionDto) { + return this.permissionService.delPermission(dto); + } +} diff --git a/packages/toolkits/pro/template/server/nestJs/src/permission/permission.guard.ts b/packages/toolkits/pro/template/server/nestJs/src/permission/permission.guard.ts new file mode 100644 index 00000000..a09cc6ec --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/permission/permission.guard.ts @@ -0,0 +1,46 @@ +import { + CanActivate, + ExecutionContext, + HttpException, + HttpStatus, + Injectable, +} from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { UserService } from '../user/user.service'; +import { Request } from 'express'; +import { User } from '@app/models'; +import { PERMISSION_KEYS } from '../public/permission.decorator'; + +interface CustomReq extends Request { + user: User; +} + +@Injectable() +export class PermissionGuard implements CanActivate { + constructor(private reflector: Reflector, private userSerivce: UserService) {} + async canActivate(ctx: ExecutionContext) { + const req: CustomReq = ctx.switchToHttp().getRequest(); + const requiredPermission = this.reflector.getAllAndOverride( + PERMISSION_KEYS, + [ctx.getClass(), ctx.getHandler()] + ); + if (!requiredPermission || requiredPermission.length === 0) { + return true; + } + const [, token] = (req.headers.authorization ?? '').split(' ') ?? ['', '']; + const permissionNames = await this.userSerivce.getUserPermission( + token, + req.user + ); + const isContainedPermission = requiredPermission.every((item) => + permissionNames.includes(item) + ); + if (permissionNames.includes('*')) { + return true; + } + if (!isContainedPermission) { + throw new HttpException('权限不足', HttpStatus.FORBIDDEN); + } + return true; + } +} diff --git a/packages/toolkits/pro/template/server/nestJs/src/permission/permission.module.ts b/packages/toolkits/pro/template/server/nestJs/src/permission/permission.module.ts new file mode 100644 index 00000000..7b018ea4 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/permission/permission.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { PermissionService } from './permission.service'; +import { PermissionController } from './permission.controller'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Permission } from '@app/models'; + +@Module({ + controllers: [PermissionController], + providers: [PermissionService], + imports: [TypeOrmModule.forFeature([Permission])], + exports: [PermissionService], +}) +export class PermissionModule {} diff --git a/packages/toolkits/pro/template/server/nestJs/src/permission/permission.service.ts b/packages/toolkits/pro/template/server/nestJs/src/permission/permission.service.ts new file mode 100644 index 00000000..6b628bd0 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/permission/permission.service.ts @@ -0,0 +1,52 @@ +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { CreatePermissionDto } from './dto/create-permission.dto'; +import { UpdatePermissionDto } from './dto/update-permission.dto'; +import { DeletePermissionDto } from './dto/delete-permission.dto'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Permission } from '@app/models'; +import { Repository } from 'typeorm'; + +@Injectable() +export class PermissionService { + constructor( + @InjectRepository(Permission) + private permission: Repository + ) {} + async create(createPermissionDto: CreatePermissionDto, isInit: boolean) { + const { name, desc } = createPermissionDto; + const permissionInfo = this.permission.findOne({ + where: { name }, + }); + if (isInit == true && (await permissionInfo)) { + return permissionInfo; + } + if ((await permissionInfo) && isInit == false) { + throw new HttpException( + `权限字段 ${name} 已经存在`, + HttpStatus.BAD_REQUEST + ); + } + const permission = await this.permission.save({ name, desc }); + return permission; + } + async updatePermission(dto: UpdatePermissionDto) { + const { name, desc, id } = dto; + const permissionInfo = await this.permission.findOne({ + where: { id }, + }); + if (!permissionInfo) { + throw new HttpException('无法找到权限字段', HttpStatus.NOT_FOUND); + } + return this.permission.update(id, { name, desc }); + } + async findPermission() { + return this.permission.find(); + } + async delPermission(deletePermissionDto: DeletePermissionDto) { + const { name } = deletePermissionDto; + const permissionInfo = await this.permission.findOne({ + where: { name }, + }); + return this.permission.remove(permissionInfo); + } +} diff --git a/packages/toolkits/pro/template/server/nestJs/src/public/permission.decorator.ts b/packages/toolkits/pro/template/server/nestJs/src/public/permission.decorator.ts new file mode 100644 index 00000000..a0fc1a5f --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/public/permission.decorator.ts @@ -0,0 +1,6 @@ +import { SetMetadata } from '@nestjs/common'; + +export const PERMISSION_KEYS = 'permissions'; + +export const Permission = (...permissions: string[]) => + SetMetadata(PERMISSION_KEYS, permissions); diff --git a/packages/toolkits/pro/template/server/nestJs/src/public/public.decorator.ts b/packages/toolkits/pro/template/server/nestJs/src/public/public.decorator.ts new file mode 100644 index 00000000..5b8bb31b --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/public/public.decorator.ts @@ -0,0 +1,3 @@ +import { SetMetadata } from '@nestjs/common'; + +export const Public = () => SetMetadata('isPublic', true); diff --git a/packages/toolkits/pro/template/server/nestJs/src/role/dto/create-role.dto.ts b/packages/toolkits/pro/template/server/nestJs/src/role/dto/create-role.dto.ts new file mode 100644 index 00000000..44232ec8 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/role/dto/create-role.dto.ts @@ -0,0 +1,12 @@ +import { IsArray, IsNotEmpty } from 'class-validator'; + +export class CreateRoleDto { + @IsNotEmpty() + name: string; + @IsArray() + @IsNotEmpty() + permissionIds: number[]; + @IsArray() + @IsNotEmpty() + menuIds: number[]; +} diff --git a/packages/toolkits/pro/template/server/nestJs/src/role/dto/delete-role.dto.ts b/packages/toolkits/pro/template/server/nestJs/src/role/dto/delete-role.dto.ts new file mode 100644 index 00000000..a53480b0 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/role/dto/delete-role.dto.ts @@ -0,0 +1,6 @@ +import { IsNotEmpty } from 'class-validator'; + +export class DeleteRoleDto { + @IsNotEmpty() + name: string; +} diff --git a/packages/toolkits/pro/template/server/nestJs/src/role/dto/update-role.dto.ts b/packages/toolkits/pro/template/server/nestJs/src/role/dto/update-role.dto.ts new file mode 100644 index 00000000..6339203f --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/role/dto/update-role.dto.ts @@ -0,0 +1,6 @@ +import { PartialType } from '@nestjs/mapped-types'; +import { CreateRoleDto } from './create-role.dto'; + +export class UpdateRoleDto extends PartialType(CreateRoleDto) { + id: number; +} diff --git a/packages/toolkits/pro/template/server/nestJs/src/role/entities/role.entity.ts b/packages/toolkits/pro/template/server/nestJs/src/role/entities/role.entity.ts new file mode 100644 index 00000000..ec816d51 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/role/entities/role.entity.ts @@ -0,0 +1 @@ +export class Role {} diff --git a/packages/toolkits/pro/template/server/nestJs/src/role/role.controller.ts b/packages/toolkits/pro/template/server/nestJs/src/role/role.controller.ts new file mode 100644 index 00000000..ce9992b1 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/role/role.controller.ts @@ -0,0 +1,43 @@ +import { + Controller, + Get, + Post, + Body, + Patch, + Param, + Delete, +} from '@nestjs/common'; +import { RoleService } from './role.service'; +import { CreateRoleDto } from './dto/create-role.dto'; +import { UpdateRoleDto } from './dto/update-role.dto'; +import { DeleteRoleDto } from './dto/delete-role.dto'; +import { Permission } from '../public/permission.decorator'; + +@Controller('role') +export class RoleController { + constructor(private readonly roleService: RoleService) {} + + @Permission('role::add') + @Post() + create(@Body() createRoleDto: CreateRoleDto) { + return this.roleService.create(createRoleDto, false); + } + + @Permission('role::get') + @Get() + getAllRole() { + return this.roleService.findAll(); + } + + @Patch() + @Permission('role::update') + updateRole(@Body() dto: UpdateRoleDto) { + return this.roleService.update(dto); + } + + @Delete() + @Permission('role::remove') + deleteRole(@Body() dto: DeleteRoleDto) { + return this.roleService.delete(dto); + } +} diff --git a/packages/toolkits/pro/template/server/nestJs/src/role/role.module.ts b/packages/toolkits/pro/template/server/nestJs/src/role/role.module.ts new file mode 100644 index 00000000..4622092a --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/role/role.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { RoleService } from './role.service'; +import { RoleController } from './role.controller'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Menu, Permission, Role } from '@app/models'; + +@Module({ + controllers: [RoleController], + providers: [RoleService], + imports: [TypeOrmModule.forFeature([Role, Permission, Menu])], + exports: [RoleService], +}) +export class RoleModule {} diff --git a/packages/toolkits/pro/template/server/nestJs/src/role/role.service.ts b/packages/toolkits/pro/template/server/nestJs/src/role/role.service.ts new file mode 100644 index 00000000..d95cb37e --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/role/role.service.ts @@ -0,0 +1,81 @@ +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { CreateRoleDto } from './dto/create-role.dto'; +import { UpdateRoleDto } from './dto/update-role.dto'; +import { DeleteRoleDto } from './dto/delete-role.dto'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Menu, Permission, Role } from '@app/models'; +import { DataSource, In, Repository } from 'typeorm'; + +@Injectable() +export class RoleService { + constructor( + @InjectRepository(Role) + private readonly role: Repository, + @InjectRepository(Permission) + private readonly permission: Repository, + @InjectRepository(Menu) + private readonly menu: Repository + ) {} + async create(createRoleDto: CreateRoleDto, isInit: boolean) { + const { name, permissionIds = [], menuIds = [] } = createRoleDto; + const roleInfo = this.role.findOne({ + where: { + name, + }, + }); + if (isInit == true && (await roleInfo)) { + return roleInfo; + } + if (await roleInfo) { + throw new HttpException('角色已存在', HttpStatus.BAD_REQUEST); + } + const permissions = await this.permission.find({ + where: { + id: In(permissionIds), + }, + }); + const menus = await this.menu.find({ + where: { + id: In(menuIds), + }, + }); + return this.role.save({ name, permission: permissions, menus }); + } + findAll() { + return this.role.find(); + } + async update(data: UpdateRoleDto) { + const permission = await this.permission.find({ + where: { + id: In(data.permissionIds ?? []), + }, + }); + const menus = await this.menu.find({ + where: { + id: In(data.menuIds ?? []), + }, + }); + const { id, name } = data; + const roleInfo = await this.role.find({ + where: { + id: id, + }, + }); + if (roleInfo.length === 0) { + throw new HttpException('角色不存在', HttpStatus.BAD_REQUEST); + } + const role = roleInfo[0]; + role.name = name; + role.permission = permission.length ? permission : undefined; + role.menus = menus.length ? menus : undefined; + return this.role.save(role); + } + async delete(data: DeleteRoleDto) { + const role = await this.role.find({ + where: { + name: data.name, + }, + }); + return this.role.remove(role); + } +} diff --git a/packages/toolkits/pro/template/server/nestJs/src/user/dto/create-user.dto.ts b/packages/toolkits/pro/template/server/nestJs/src/user/dto/create-user.dto.ts new file mode 100644 index 00000000..c4d34b34 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/user/dto/create-user.dto.ts @@ -0,0 +1,17 @@ +import { IsNotEmpty } from 'class-validator'; + +export class CreateUserDto { + @IsNotEmpty({ + message: '用户名不能为空', + }) + username: string; + @IsNotEmpty({ + message: '邮箱不能为空', + }) + email: string; + @IsNotEmpty({ + message: '密码不能为空', + }) + password: string; + roleIds: number[] = []; +} diff --git a/packages/toolkits/pro/template/server/nestJs/src/user/dto/update-user.dto.ts b/packages/toolkits/pro/template/server/nestJs/src/user/dto/update-user.dto.ts new file mode 100644 index 00000000..dd83a3aa --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/user/dto/update-user.dto.ts @@ -0,0 +1,14 @@ +import { PartialType } from '@nestjs/mapped-types'; +import { CreateUserDto } from './create-user.dto'; +import { IsNotEmpty } from 'class-validator'; + +export class UpdateUserDto extends PartialType(CreateUserDto) { + @IsNotEmpty({ + message: '旧密码不能为空', + }) + oldPassword: string; + @IsNotEmpty({ + message: '新密码不能为空', + }) + newPassword: string; +} diff --git a/packages/toolkits/pro/template/server/nestJs/src/user/user.controller.spec.ts b/packages/toolkits/pro/template/server/nestJs/src/user/user.controller.spec.ts new file mode 100644 index 00000000..1f38440d --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/user/user.controller.spec.ts @@ -0,0 +1,20 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { UserController } from './user.controller'; +import { UserService } from './user.service'; + +describe('UserController', () => { + let controller: UserController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [UserController], + providers: [UserService], + }).compile(); + + controller = module.get(UserController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/packages/toolkits/pro/template/server/nestJs/src/user/user.controller.ts b/packages/toolkits/pro/template/server/nestJs/src/user/user.controller.ts new file mode 100644 index 00000000..59a5dad9 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/user/user.controller.ts @@ -0,0 +1,39 @@ +import { + Body, + Controller, + Delete, + Get, + Param, + Patch, + Post, + Put, + Query, +} from '@nestjs/common'; +import { UserService } from './user.service'; +import { CreateUserDto } from './dto/create-user.dto'; +import { Permission } from '../public/permission.decorator'; +import { UpdateUserDto } from './dto/update-user.dto'; + +@Controller('user') +export class UserController { + constructor(private readonly userService: UserService) {} + @Post('reg') + @Permission('user::add') + async register(@Body() body: CreateUserDto) { + return this.userService.create(body, false); + } + @Get('/info/:email') + async getUserInfo(@Param('email') email: string) { + return this.userService.getUserInfo(email, ['role', 'role.permission']); + } + @Delete('/:email') + @Permission('user::remove') + async delUser(@Param('email') email: string) { + return this.userService.deleteUser(email); + } + @Patch('/update') + @Permission('user:update') + async UpdateUser(@Body() body: UpdateUserDto) { + return this.userService.updateUserPwd(body); + } +} diff --git a/packages/toolkits/pro/template/server/nestJs/src/user/user.module.ts b/packages/toolkits/pro/template/server/nestJs/src/user/user.module.ts new file mode 100644 index 00000000..efcdfde2 --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/user/user.module.ts @@ -0,0 +1,15 @@ +import { Module } from '@nestjs/common'; +import { UserService } from './user.service'; +import { UserController } from './user.controller'; +import { Permission, Role, User } from '@app/models'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { AuthService } from '../auth/auth.service'; +import { RedisService } from '../../libs/redis/redis.service'; + +@Module({ + imports: [TypeOrmModule.forFeature([User, Permission, Role])], + controllers: [UserController], + providers: [UserService, AuthService, RedisService], + exports: [UserService], +}) +export class UserModule {} diff --git a/packages/toolkits/pro/template/server/nestJs/src/user/user.service.spec.ts b/packages/toolkits/pro/template/server/nestJs/src/user/user.service.spec.ts new file mode 100644 index 00000000..873de8ac --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/user/user.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { UserService } from './user.service'; + +describe('UserService', () => { + let service: UserService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [UserService], + }).compile(); + + service = module.get(UserService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/packages/toolkits/pro/template/server/nestJs/src/user/user.service.ts b/packages/toolkits/pro/template/server/nestJs/src/user/user.service.ts new file mode 100644 index 00000000..40d5df5d --- /dev/null +++ b/packages/toolkits/pro/template/server/nestJs/src/user/user.service.ts @@ -0,0 +1,140 @@ +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { CreateUserDto } from './dto/create-user.dto'; +import { UpdateUserDto } from './dto/update-user.dto'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Role, User } from '@app/models'; +import { In, Repository } from 'typeorm'; +import * as crypto from 'crypto'; +import { AuthService } from '../auth/auth.service'; + +@Injectable() +export class UserService { + constructor( + @InjectRepository(User) + private userRep: Repository, + @InjectRepository(Role) + private roleRep: Repository, + private readonly authService: AuthService + ) {} + async create(createUserDto: CreateUserDto, isInit: boolean) { + const { email, password, roleIds = [], username } = createUserDto; + const userInfo = this.getUserInfo(email); + if (isInit == true && (await userInfo)) { + return userInfo; + } + if (await userInfo) { + throw new HttpException('用户存在', HttpStatus.BAD_REQUEST); + } + const roles = this.roleRep.find({ + where: { + id: In(roleIds), + }, + }); + try { + const user = this.userRep.create({ + email, + password, + name: username, + role: await roles, + }); + return this.userRep.save(user); + } catch (err) { + throw new HttpException( + (err as Error).message, + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + } + + async getUserInfo(email: string, relations: string[] = []) { + return this.userRep.findOne({ + where: { email, deleteAt: null }, + select: [ + 'id', + 'name', + 'email', + 'createTime', + 'updateTime', + 'role', + 'deleteAt', + ], + relations, + }); + } + + async getUserPermission(token: string, userInfo: User) { + const { email } = userInfo; + const { role } = (await this.getUserInfo(email, [ + 'role', + 'role.permission', + ])) ?? { role: [] as Role[] }; + const permission = role.flatMap((r) => r.permission); + const permissionNames = permission.map((p) => p.name); + return [...new Set([...permissionNames])]; + } + + //验证旧密码是否正确 + async verifyPassword(password: string, storedHash: string, salt: string) { + const newHash = crypto + .pbkdf2Sync(password, salt, 1000, 18, 'sha256') + .toString('hex'); + return newHash === storedHash; + } + //修改密码后加密 + async encry(value: string, salt: string) { + return crypto.pbkdf2Sync(value, salt, 1000, 18, 'sha256').toString('hex'); + } + + async deleteUser(email: string) { + const user = await this.getUserInfo(email); + if (user) { + user.deleteAt = new Date().getTime(); // 设置软删除字段 + await this.userRep.save(user); + return; + } + } + + //修改密码 + async updateUserPwd(updateUserDto: UpdateUserDto) { + const { email, newPassword, oldPassword } = updateUserDto; + const user = this.userRep.findOne({ + where: { email, deleteAt: null }, + select: [ + 'id', + 'name', + 'email', + 'salt', + 'password', + 'createTime', + 'updateTime', + 'role', + 'deleteAt', + ], + }); + if (user) { + if ( + !(await this.verifyPassword( + oldPassword, + ( + await user + ).password, + ( + await user + ).salt + )) + ) { + throw new HttpException('旧密码错误', HttpStatus.BAD_REQUEST); + } else { + (await user).password = await this.encry( + newPassword, + ( + await user + ).salt + ); + await this.userRep.save(await user); + await this.authService.logout(email); + return; + } + } + } +} diff --git a/packages/toolkits/pro/template/server/nestJs/tsconfig.json b/packages/toolkits/pro/template/server/nestJs/tsconfig.json index d9c82ca9..aa86624c 100644 --- a/packages/toolkits/pro/template/server/nestJs/tsconfig.json +++ b/packages/toolkits/pro/template/server/nestJs/tsconfig.json @@ -11,7 +11,21 @@ "outDir": "./dist", "baseUrl": "./", "incremental": true, - "skipLibCheck": true + "skipLibCheck": true, + "paths": { + "@app/db": [ + "libs/db/src" + ], + "@app/db/*": [ + "libs/db/src/*" + ], + "@app/models": [ + "libs/models/src" + ], + "@app/models/*": [ + "libs/models/src/*" + ] + } }, "include": ["src/**/*"] }