diff --git a/package.json b/package.json index 95bdea2..3819e36 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "colors": "^1.4.0", "drizzle-orm": "^0.31.2", "fastify": "^4.27.0", + "jsonwebtoken": "^9.0.2", "mysql2": "^3.10.0", "nodemailer": "^6.9.13", "redis": "^4.6.14", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 18e06ef..02ff2c1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -47,6 +47,9 @@ importers: fastify: specifier: ^4.27.0 version: 4.27.0 + jsonwebtoken: + specifier: ^9.0.2 + version: 9.0.2 mysql2: specifier: ^3.10.0 version: 3.10.0 @@ -1398,6 +1401,9 @@ packages: bser@2.1.1: resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} @@ -1798,6 +1804,9 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} @@ -2569,6 +2578,16 @@ packages: jsonfile@6.1.0: resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + jsonwebtoken@9.0.2: + resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} + engines: {node: '>=12', npm: '>=6'} + + jwa@1.4.1: + resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==} + + jws@3.2.2: + resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -2608,12 +2627,33 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + + lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + + lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + lodash.memoize@4.1.2: resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} @@ -5117,6 +5157,8 @@ snapshots: dependencies: node-int64: 0.4.0 + buffer-equal-constant-time@1.0.1: {} + buffer-from@1.1.2: {} buffer@5.7.1: @@ -5413,6 +5455,10 @@ snapshots: eastasianwidth@0.2.0: {} + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + ee-first@1.1.1: {} electron-to-chromium@1.4.789: {} @@ -6502,6 +6548,30 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 + jsonwebtoken@9.0.2: + dependencies: + jws: 3.2.2 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.6.2 + + jwa@1.4.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@3.2.2: + dependencies: + jwa: 1.4.1 + safe-buffer: 5.2.1 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -6537,10 +6607,24 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash.includes@4.3.0: {} + + lodash.isboolean@3.0.3: {} + + lodash.isinteger@4.0.4: {} + + lodash.isnumber@3.0.3: {} + + lodash.isplainobject@4.0.6: {} + + lodash.isstring@4.0.1: {} + lodash.memoize@4.1.2: {} lodash.merge@4.6.2: {} + lodash.once@4.1.1: {} + lodash@4.17.21: {} log-symbols@4.1.0: diff --git a/src/application/auth-dept/auth-dept.service.ts b/src/application/auth-dept/auth-dept.service.ts index 3ce83ec..70e2948 100644 --- a/src/application/auth-dept/auth-dept.service.ts +++ b/src/application/auth-dept/auth-dept.service.ts @@ -166,7 +166,6 @@ export class AuthDeptService { // ? 存在正在进行写入的部门 if (!lock) throw new HttpException('服务繁忙,部门名称重复!', HttpStatus.CONFLICT); - try { // ! 同级查重 const result = await this.checkRepeatForDeptName(updateAuthDeptDto.deptName, updateAuthDeptDto.pid); diff --git a/src/application/auth-post/auth-post.controller.ts b/src/application/auth-post/auth-post.controller.ts index 428df55..83b1991 100644 --- a/src/application/auth-post/auth-post.controller.ts +++ b/src/application/auth-post/auth-post.controller.ts @@ -1,11 +1,11 @@ -import {Controller, Get, Post, Body, Patch, Param, Delete, Query} from '@nestjs/common'; +import { Controller, Get, Post, Body, Patch, Param, Delete, Query } from '@nestjs/common'; import { AuthPostService } from './auth-post.service'; import { CreateAuthPostDto } from './dto/create-auth-post.dto'; import { UpdateAuthPostDto } from './dto/update-auth-post.dto'; import { ApiOperation, ApiProduces, ApiTags } from '@nestjs/swagger'; import { PacInfo } from '@common/decorator/pac-info/pac-info.decorator'; import { PacInfoType } from '@utils/myType'; -import {GetAuthPostDto} from "@app/auth-post/dto/get-auth-post.dto"; +import { GetAuthPostDto } from '@app/auth-post/dto/get-auth-post.dto'; @ApiTags('岗位服务') @Controller('authPost') @@ -18,7 +18,6 @@ export class AuthPostController { }) @ApiProduces('application/json') @Post() - @Post() create(@Body() createAuthPostDto: CreateAuthPostDto, @PacInfo() pacInfo: PacInfoType) { return this.authPostService.create(createAuthPostDto, pacInfo); } diff --git a/src/application/auth-post/auth-post.module.ts b/src/application/auth-post/auth-post.module.ts index 8a33633..6c81b07 100644 --- a/src/application/auth-post/auth-post.module.ts +++ b/src/application/auth-post/auth-post.module.ts @@ -3,7 +3,7 @@ import { AuthPostService } from './auth-post.service'; import { AuthPostController } from './auth-post.controller'; @Module({ - controllers: [AuthPostController], - providers: [AuthPostService], + controllers: [AuthPostController], + providers: [AuthPostService], }) export class AuthPostModule {} diff --git a/src/application/auth-post/auth-post.service.ts b/src/application/auth-post/auth-post.service.ts index 3804a29..4d0e853 100644 --- a/src/application/auth-post/auth-post.service.ts +++ b/src/application/auth-post/auth-post.service.ts @@ -6,7 +6,7 @@ import { MysqlService } from '@common/service/mysql/mysql.service'; import { RedisService } from '@common/service/redis/redis.service'; import { Snowflake } from '@service/snowflake/snowflake.service'; import { ConfigService } from '@nestjs/config'; -import {pacAuthDept, pacAuthPost, pacAuthUser, pacCoreDict} from '@entities/schema'; +import { pacAuthDept, pacAuthPost, pacAuthUser, pacCoreDict } from '@entities/schema'; import { and, asc, desc, eq, isNull, like, or, sql } from 'drizzle-orm'; import { PacInfoType } from '@utils/myType'; import { isExistKey, isTrueEnum } from '@utils/boolean.enum'; @@ -266,7 +266,6 @@ export class AuthPostService { // ! 查重 const result = await this.checkRepeatForPostKey(updateAuthPostDto.postKey); - // ? 是否存在重复的岗位 if (result.length > 0 && result[0].id != id) throw new HttpException('岗位标识重复!', HttpStatus.CONFLICT); diff --git a/src/application/auth-role/auth-role.controller.ts b/src/application/auth-role/auth-role.controller.ts index 08e64e4..a0098ca 100644 --- a/src/application/auth-role/auth-role.controller.ts +++ b/src/application/auth-role/auth-role.controller.ts @@ -7,7 +7,7 @@ import { PacInfo } from '@common/decorator/pac-info/pac-info.decorator'; import { PacInfoType } from '@utils/myType'; import { query } from 'express'; import { GetPacAuthRoleAllDto } from '@app/auth-role/dto/get-auth-role.dto'; -import {GetUserForAttDto} from "@dto/GetUserForAtt.dto"; +import { GetUserForAttDto } from '@dto/GetUserForAtt.dto'; import { RoleLinkUserDto } from '@dto/AttLinkUser.dto'; @ApiTags('角色服务') diff --git a/src/application/auth-role/auth-role.service.ts b/src/application/auth-role/auth-role.service.ts index 244703f..b857201 100644 --- a/src/application/auth-role/auth-role.service.ts +++ b/src/application/auth-role/auth-role.service.ts @@ -529,7 +529,7 @@ export class AuthRoleService { throw new HttpException('该角色存在子项!', HttpStatus.BAD_REQUEST); } - // ! 清角色菜单关联表 + // ! 清理角色菜单关联表 await this.mysqlService.db.delete(pacAuthLinkRoleMenu).where(eq(pacAuthLinkRoleMenu.roleId as any, roleId)); // ? 判断是否是自定义数据范围,清角色部门关联表 diff --git a/src/application/auth-user/auth-user.controller.ts b/src/application/auth-user/auth-user.controller.ts index b4b5a63..cbc64d3 100644 --- a/src/application/auth-user/auth-user.controller.ts +++ b/src/application/auth-user/auth-user.controller.ts @@ -1,34 +1,127 @@ -import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common'; +import { Controller, Get, Post, Body, Patch, Param, Delete, Query } from '@nestjs/common'; import { AuthUserService } from './auth-user.service'; import { CreateAuthUserDto } from './dto/create-auth-user.dto'; import { UpdateAuthUserDto } from './dto/update-auth-user.dto'; +import { ApiOperation, ApiProduces, ApiTags } from '@nestjs/swagger'; +import { PacInfo } from '@common/decorator/pac-info/pac-info.decorator'; +import { PacInfoType } from '@utils/myType'; +import { GetPacAuthUserAllDto } from '@app/auth-user/dto/get-auth-user.dto'; +import { PasswordDto } from '@app/auth-user/dto/password.dto'; +import { UpdateLinkDto } from '@app/auth-user/dto/updateLink.dto'; +import {UsernameSignInDto} from "@app/auth-user/dto/signin.dto"; -@Controller('auth-user') +@ApiTags('账户服务') +@Controller('authUser') export class AuthUserController { - constructor(private readonly authUserService: AuthUserService) {} - - @Post() - create(@Body() createAuthUserDto: CreateAuthUserDto) { - return this.authUserService.create(createAuthUserDto); - } - - @Get() - findAll() { - return this.authUserService.findAll(); - } - - @Get(':id') - findOne(@Param('id') id: string) { - return this.authUserService.findOne(+id); - } - - @Patch(':id') - update(@Param('id') id: string, @Body() updateAuthUserDto: UpdateAuthUserDto) { - return this.authUserService.update(+id, updateAuthUserDto); - } - - @Delete(':id') - remove(@Param('id') id: string) { - return this.authUserService.remove(+id); - } + constructor(private readonly authUserService: AuthUserService) {} + + @ApiOperation({ + summary: '添加账户', + description: '账户', + }) + @ApiProduces('application/json') + @Post() + create(@Body() createAuthUserDto: CreateAuthUserDto, @PacInfo() pacInfo: PacInfoType) { + return this.authUserService.create(createAuthUserDto, pacInfo); + } + + @ApiOperation({ + summary: '获取账户列表', + description: '查询账户分页或者列表', + }) + @ApiProduces('application/json') + @Get() + findAll(@Query() getPacAuthUserAllDto: GetPacAuthUserAllDto) { + return this.authUserService.findAll(getPacAuthUserAllDto); + } + + @ApiOperation({ + summary: '获取账户详细信息', + description: '查询账户详细信息,目录菜单列表,数据权限范围', + }) + @ApiProduces('application/json') + @Get(':id') + findOne(@Param('id') id: string) { + return this.authUserService.findOne(id); + } + + @ApiOperation({ + summary: '更新账户信息', + description: '更新账户信息', + }) + @ApiProduces('application/json') + @Patch(':id') + update(@Param('id') id: string, @Body() updateAuthUserDto: UpdateAuthUserDto, @PacInfo() pacInfo: PacInfoType) { + return this.authUserService.update(id, updateAuthUserDto, pacInfo); + } + + @ApiOperation({ + summary: '删除目标账户', + description: '删除目标账户信息', + }) + @ApiProduces('application/json') + @Delete(':id') + remove(@Param('id') id: string, @PacInfo() pacInfo: PacInfoType) { + return this.authUserService.remove(id, pacInfo); + } + + @ApiOperation({ + summary: '重置密码', + description: '给指定用户将密码恢复为系统默认账户密码', + }) + @ApiProduces('application/json') + @Post('/password/:id') + resetTargetUserPassword(@Param('id') id: string) { + return this.authUserService.resetTargetUserPassword(id); + } + + @ApiOperation({ + summary: '修改密码', + description: '修改自己密码', + }) + @ApiProduces('application/json') + @Patch('/password') + updatePassword(@Param('id') id: string, @Body() passwordDto: PasswordDto) { + return this.authUserService.updatePassword(id, passwordDto); + } + + @ApiOperation({ + summary: '更新账户角色', + description: '更新账户角色', + }) + @ApiProduces('application/json') + @Patch('/role/:id') + updateLinkRole(@Param('id') id: string, @Body() updateLinkDto: UpdateLinkDto, @PacInfo() pacInfo: PacInfoType) { + return this.authUserService.updateLinkRole(id, updateLinkDto, pacInfo); + } + + @ApiOperation({ + summary: '更新账户部门', + description: '更新账户部门', + }) + @ApiProduces('application/json') + @Patch('/dept/:id') + updateLinkDept(@Param('id') id: string, @Body() updateLinkDto: UpdateLinkDto, @PacInfo() pacInfo: PacInfoType) { + return this.authUserService.updateLinkDept(id, updateLinkDto, pacInfo); + } + + @ApiOperation({ + summary: '更新账户岗位', + description: '更新账户岗位', + }) + @ApiProduces('application/json') + @Patch('/post/:id') + updateLinkPost(@Param('id') id: string, @Body() updateLinkDto: UpdateLinkDto, @PacInfo() pacInfo: PacInfoType) { + return this.authUserService.updateLinkPost(id, updateLinkDto, pacInfo); + } + + @ApiOperation({ + summary: '登陆系统', + description: '登陆系统', + }) + @ApiProduces('application/json') + @Post('/signin') + signin(@Body() usernameSignInDto: UsernameSignInDto) { + return this.authUserService.signin(usernameSignInDto); + } } diff --git a/src/application/auth-user/auth-user.module.ts b/src/application/auth-user/auth-user.module.ts index f6884bb..f1ba91e 100644 --- a/src/application/auth-user/auth-user.module.ts +++ b/src/application/auth-user/auth-user.module.ts @@ -1,9 +1,10 @@ import { Module } from '@nestjs/common'; import { AuthUserService } from './auth-user.service'; import { AuthUserController } from './auth-user.controller'; +import { JwtService } from '@common/service/jwt/jwt.service'; @Module({ - controllers: [AuthUserController], - providers: [AuthUserService], + controllers: [AuthUserController], + providers: [AuthUserService, JwtService], }) export class AuthUserModule {} diff --git a/src/application/auth-user/auth-user.service.ts b/src/application/auth-user/auth-user.service.ts index 31151fb..a921de7 100644 --- a/src/application/auth-user/auth-user.service.ts +++ b/src/application/auth-user/auth-user.service.ts @@ -1,26 +1,696 @@ -import { Injectable } from '@nestjs/common'; +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { CreateAuthUserDto } from './dto/create-auth-user.dto'; import { UpdateAuthUserDto } from './dto/update-auth-user.dto'; +import { PacInfoType } from '@utils/myType'; +import { LoggerService } from '@service/logger/logger.service'; +import { MysqlService } from '@common/service/mysql/mysql.service'; +import { RedisService } from '@common/service/redis/redis.service'; +import { Snowflake } from '@service/snowflake/snowflake.service'; +import { ConfigService } from '@nestjs/config'; +import { + pacAuthDept, + pacAuthLinkRoleMenu, + pacAuthLinkUserDept, + pacAuthLinkUserPost, + pacAuthLinkUserRole, + pacAuthPost, + pacAuthRole, + pacAuthUser, + pacCoreDict, +} from '@entities/schema'; +import { and, asc, desc, eq, gt, isNull, like, or, sql } from 'drizzle-orm'; +import cryptoPassword from '@utils/cryptoPassword'; +import { GetPacAuthUserAllDto } from '@app/auth-user/dto/get-auth-user.dto'; +import { isExistKey, isTrueEnum } from '@utils/boolean.enum'; +import { likeQuery } from '@utils/likeQuery'; +import { alias } from 'drizzle-orm/mysql-core'; +import { from } from 'rxjs'; +import { UpdateCoreDictDto } from '@app/core-dict/dto/update-core-dict.dto'; +import { PasswordDto } from '@app/auth-user/dto/password.dto'; +import { UpdateLinkDto } from '@app/auth-user/dto/updateLink.dto'; +import { UsernameSignInDto } from '@app/auth-user/dto/signin.dto'; +import { JwtService } from '@common/service/jwt/jwt.service'; +import MD5 from '@utils/MD5'; @Injectable() export class AuthUserService { - create(createAuthUserDto: CreateAuthUserDto) { - return 'This action adds a new authUser'; - } + // 分页数据 + private readonly userPageType = { + userId: pacAuthUser.userId, + username: pacAuthUser.username, + nickname: pacAuthUser.nickname, + userType: pacAuthUser.userType, + userTypeName: pacCoreDict.dictName, + userTypeKey: pacCoreDict.dictKey, + userEmail: pacAuthUser.userEmail, + pid: pacAuthUser.pid, + wxAppid: pacAuthUser.wxAppid, + avatar: pacAuthUser.avatar, + userPhone: pacAuthUser.userPhone, + userDesc: pacAuthUser.userDesc, + status: pacAuthUser.status, + haveChildren: pacAuthUser.haveChildren, + createby: pacAuthUser.createby, + createtime: pacAuthUser.createtime, + updateby: pacAuthUser.updateby, + updatetime: pacAuthUser.updatetime, + }; - findAll() { - return `This action returns all authUser`; - } + // 列表数据 + private readonly userListType = { + userId: pacAuthUser.userId, + username: pacAuthUser.username, + nickname: pacAuthUser.nickname, + userType: pacAuthUser.userType, + userTypeName: pacCoreDict.dictName, + userTypeKey: pacCoreDict.dictKey, + }; + constructor( + private readonly logger: LoggerService, + private readonly mysqlService: MysqlService, + private readonly redisService: RedisService, + private readonly snowflake: Snowflake, + private readonly config: ConfigService, + private readonly jwt: JwtService, + ) {} - findOne(id: number) { - return `This action returns a #${id} authUser`; - } + /** Service + * NAME: create + * DESC: 创建账户 + * DATE: 2024-06-29 13:13:55 - + * */ + public async create(createAuthUserDto: CreateAuthUserDto, pacInfo: PacInfoType) { + // ! 加目标锁,同级,而不是全局 + const lock = await this.redisService.distributedLock('USER' + createAuthUserDto.username, createAuthUserDto.username); - update(id: number, updateAuthUserDto: UpdateAuthUserDto) { - return `This action updates a #${id} authUser`; - } + // ? 存在正在进行写入的账户 + if (!lock) throw new HttpException('服务繁忙,账户名称重复!', HttpStatus.CONFLICT); - remove(id: number) { - return `This action removes a #${id} authUser`; - } + try { + // ! 用户名查重 + const result = await this.checkRepeatForUsername(createAuthUserDto.username); + + // ? 是否存在重复的账户 + if (result.length > 0) throw new HttpException('用户名重复!', HttpStatus.CONFLICT); + + // ! 添加账户数据 + const newPacCoreDict = await this.addUser(createAuthUserDto, pacInfo); + + // ! 解锁 + lock(); + + const insertList = []; + + // ! 关联角色 + if (createAuthUserDto.roleList && createAuthUserDto.roleList.length > 0) { + insertList.push(this.updateUserRole(newPacCoreDict.userId as any, createAuthUserDto.roleList, pacInfo)); + } + + // ! 关联部门 + if (createAuthUserDto.deptList && createAuthUserDto.deptList.length > 0) { + insertList.push(this.updateUserDept(newPacCoreDict.userId as any, createAuthUserDto.deptList, pacInfo)); + } + + // ! 关联岗位 + if (createAuthUserDto.postList && createAuthUserDto.postList.length > 0) { + insertList.push(this.updateUserPost(newPacCoreDict.userId as any, createAuthUserDto.postList, pacInfo)); + } + + // ! 执行关联 + await Promise.all(insertList).catch((e) => { + this.logger.error('新增用户时,关联角色、部门、岗位出现错误'); + this.logger.error(e); + }); + + // !更新父节点的子节点数量 + await this.haveChildrenSelfIncreasing(createAuthUserDto.pid); + + // ! 返回结果 + return newPacCoreDict; + } catch (e) { + // ! 解锁 + lock(); + + // ! 抛出错误 + throw e; + } + + return 'This action adds a new authUser'; + } + + /** Service + * NAME: findAll + * DESC: 查找用户分页/列表 + * DATE: 2024-06-29 13:13:55 - + * */ + public async findAll(getPacAuthUserAllDto: GetPacAuthUserAllDto) { + if (isTrueEnum(getPacAuthUserAllDto['isList'])) { + return await this.getList(getPacAuthUserAllDto); + } else { + return await this.getPage(getPacAuthUserAllDto); + } + } + + /** Service + * NAME: findOne + * DESC: 查用户详细信息 + * DATE: 2024-06-29 13:13:55 - + * */ + public async findOne(id: string) { + return await this.getMore(id); + } + + /** Service + * NAME: update + * DESC: 修改账户信息 + * DATE: 2024-06-29 13:13:55 - + * */ + public update(id: string, updateAuthUserDto: UpdateAuthUserDto, pacInfo: PacInfoType) { + return this.updateUser(id, updateAuthUserDto, pacInfo); + } + + /** Service + * NAME: remove + * DESC: 移除账户 + * DATE: 2024-06-29 13:13:55 - + * */ + public remove(id: string, pacInfo: PacInfoType) { + return this.deleteUser(id, pacInfo.userId as any); + } + + /** Service + * NAME: resetTargetUserPassword + * DESC: 充值目标账户密码 + * DATE: 2024-06-29 13:13:55 - + * */ + public async resetTargetUserPassword(id: string) { + return this.resetPassword(id, null); + } + + /** Service + * NAME: updatePassword + * DESC: 修改自己账户密码 + * DATE: 2024-06-29 13:13:55 - + * */ + public async updatePassword(id: string, passwordDto: PasswordDto) { + const user = await this.mysqlService.db + .select({ + password: pacAuthUser.password, + }) + .from(pacAuthUser) + .where(eq(pacAuthUser.userId, id)); + if (user.length == 0) throw new HttpException('未找到目标用户信息,无法修改密码!', HttpStatus.BAD_REQUEST); + const oldPassword = cryptoPassword(passwordDto.oldPassword, this.config.get('system.passwordSalt')); + if (oldPassword != user[0].password) throw new HttpException('原密码错误!', HttpStatus.BAD_REQUEST); + return this.resetPassword(id, passwordDto.newPassword); + } + + /** Service + * NAME: updatePassword + * DESC: 修改目标账户角色关联 + * DATE: 2024-06-29 13:13:55 - + * */ + public async updateLinkRole(id: string, updateLinkDto: UpdateLinkDto, pacInfo: PacInfoType) { + await this.checkUserExist(id); + if (updateLinkDto) { + return this.updateUserRole(id, updateLinkDto.list, pacInfo); + } else { + return; + } + } + + /** Service + * NAME: updateLinkDept + * DESC: 修改目标账户部门关联 + * DATE: 2024-06-29 13:13:55 - + * */ + public async updateLinkDept(id: string, updateLinkDto: UpdateLinkDto, pacInfo: PacInfoType) { + await this.checkUserExist(id); + if (updateLinkDto) { + return this.updateUserDept(id, updateLinkDto.list, pacInfo); + } else { + return; + } + } + + /** Service + * NAME: updateLinkPost + * DESC: 修改目标账户岗位关联 + * DATE: 2024-06-29 13:13:55 - + * */ + public async updateLinkPost(id: string, updateLinkDto: UpdateLinkDto, pacInfo: PacInfoType) { + await this.checkUserExist(id); + if (updateLinkDto) { + return this.updateUserPost(id, updateLinkDto.list, pacInfo); + } else { + return; + } + } + + /** Service + * NAME: updateLinkPost + * DESC: 修改目标账户岗位关联 + * DATE: 2024-06-29 13:13:55 - + * */ + public async signin(usernameSignInDto: UsernameSignInDto) { + // 登陆检测 + return await this.signinDetection(usernameSignInDto); + } + + // DB 同级查重 + private checkRepeatForUsername(username: string) { + return this.mysqlService.db + .select({ + id: pacAuthUser.username, + }) + .from(pacAuthUser) + .where(and(isNull(pacAuthUser.deleteby), eq(pacAuthUser.username, username))); + } + + // DB 更新父级子元素数量 + private async haveChildrenSelfIncreasing(id: string, isAdd = true) { + return this.mysqlService.db + .update(pacAuthUser) + .set({ + haveChildren: isAdd ? sql`${pacAuthUser.haveChildren} + 1` : sql`${pacAuthUser.haveChildren} - 1`, + }) + .where(eq(pacAuthUser.userId as any, id)); + } + + // DB 添加账户 + private async addUser(createAuthUserDto: CreateAuthUserDto, pacInfo: PacInfoType) { + // ! 生成雪花id,用于账户id + const id = await this.snowflake.generate(); + + let password = ''; + + // ! 从Redis获取默认密码 + const defaultPasswordForRedis = await this.redisService.get('ENV_DEFAULT_PASSWORD'); + + // ? 判断redis中是否存在默认密码环境变量 + if (!defaultPasswordForRedis) { + password = this.config.get('system.defaultPassword'); + } else { + password = defaultPasswordForRedis; + } + + // ! 加密密码 + const pass = cryptoPassword(password, this.config.get('system.passwordSalt')); + + // ! 定义写入的账户数据 + const newUserData: typeof pacAuthUser.$inferInsert = { + userId: id as any, + username: createAuthUserDto.username, + password: pass, + nickname: createAuthUserDto.nickname, + userType: createAuthUserDto.userType, + userEmail: createAuthUserDto.userEmail, + pid: createAuthUserDto.pid, + avatar: createAuthUserDto.avatar, + userPhone: createAuthUserDto.userPhone, + userDesc: createAuthUserDto.userDesc, + createby: pacInfo.userId as any, + createtime: sql`now()` as any, + }; + return { + insert: await this.mysqlService.db.insert(pacAuthUser).values(newUserData), + userId: id.toString(), + }; + } + + // COMMON 去重数组 + private deduplicateArray(arr: string[]) { + return Array.from(new Set(arr)); + } + + // COMMON 向目标表插入列 + private async insertKeyValueForUserLink(table, list, key, userId, pacInfo: PacInfoType) { + // ! 清空曾经的关联 + await this.mysqlService.db.delete(table).where(eq(table.userId as any, userId)); + + // ! 去重列表 + const arr = this.deduplicateArray(list); + + // ! 增加新的关联 + return await this.mysqlService.db.insert(table).values( + arr.map((i) => ({ + userId: userId, + [key]: i, + createby: pacInfo.userId as any, + createtime: sql`now()` as any, + })), + ); + } + + // DB 修改用户角色 + private async updateUserRole(userId: string, roleIdList: string[], pacInfo: PacInfoType) { + return await this.insertKeyValueForUserLink(pacAuthLinkUserRole, roleIdList, 'roleId', userId, pacInfo); + } + + // DB 修改账户部门 + private async updateUserDept(userId: string, deptIdList: string[], pacInfo: PacInfoType) { + return await this.insertKeyValueForUserLink(pacAuthLinkUserDept, deptIdList, 'deptId', userId, pacInfo); + } + + // DB修改账户岗位 + private async updateUserPost(userId: string, postIdList: string[], pacInfo: PacInfoType) { + return await this.insertKeyValueForUserLink(pacAuthLinkUserPost, postIdList, 'postId', userId, pacInfo); + } + + // 查询构建器 + private queryBuilder(getPacAuthUserAllDto: GetPacAuthUserAllDto, selectData) { + // ! 定义基础查询函数 + // 启用动态查询模式 $dynamic + const query = this.mysqlService.db + .select(selectData) + .from(pacAuthUser) + .orderBy(isTrueEnum(getPacAuthUserAllDto.isAsc) ? asc(pacAuthUser.userId) : desc(pacAuthUser.userId)) + .leftJoin(pacCoreDict, eq(pacAuthUser.userType, pacCoreDict.dictId)) + + // 角色 + .leftJoin(pacAuthLinkUserRole, eq(pacAuthUser.userId, pacAuthLinkUserRole.userId)) + + // 部门 + .leftJoin(pacAuthLinkUserDept, eq(pacAuthUser.userId, pacAuthLinkUserDept.userId)) + + // 岗位 + .leftJoin(pacAuthLinkUserPost, eq(pacAuthUser.userId, pacAuthLinkUserPost.userId)) + .$dynamic(); + + // 查询条件集合 + const wl = []; + + // ? 未删除 + wl.push(isNull(pacAuthUser.deleteby)); + + // ? 模糊查询 + wl.push( + or( + like(pacAuthUser.username, likeQuery(getPacAuthUserAllDto.userInfo)), + like(pacAuthUser.nickname, likeQuery(getPacAuthUserAllDto.userInfo)), + like(pacAuthUser.userEmail, likeQuery(getPacAuthUserAllDto.userInfo)), + like(pacAuthUser.userPhone, likeQuery(getPacAuthUserAllDto.userInfo)), + like(pacAuthUser.userDesc, likeQuery(getPacAuthUserAllDto.userInfo)), + ).if(isExistKey(getPacAuthUserAllDto, 'userInfo')), + ); + + // ? 按照层级查 + wl.push(eq(pacAuthUser.pid, getPacAuthUserAllDto.hierarchy).if(isExistKey(getPacAuthUserAllDto, 'hierarchy'))); + + // ? 是否查角色类型 + wl.push(eq(pacAuthUser.userType, getPacAuthUserAllDto.userType).if(isExistKey(getPacAuthUserAllDto, 'userType'))); + + // ? 是否查字典状态 + wl.push(eq(pacAuthUser.status, getPacAuthUserAllDto.status as any).if(isExistKey(getPacAuthUserAllDto, 'status'))); + + // ? 是否存在子账户 + wl.push(gt(pacAuthUser.haveChildren, 0).if(isExistKey(getPacAuthUserAllDto, 'haveChildren') && isTrueEnum(getPacAuthUserAllDto.haveChildren))); + + // ? 角色 + wl.push( + eq(pacAuthLinkUserRole.roleId, getPacAuthUserAllDto.roleId).if( + isExistKey(getPacAuthUserAllDto, 'roleId') && !isTrueEnum(getPacAuthUserAllDto.noRole), + ), + ); + + // ? 部门 + wl.push(eq(pacAuthLinkUserDept.deptId, getPacAuthUserAllDto.deptId).if(isExistKey(getPacAuthUserAllDto, 'deptId'))); + + // ? 岗位 + wl.push(eq(pacAuthLinkUserPost.postId, getPacAuthUserAllDto.postId).if(isExistKey(getPacAuthUserAllDto, 'postId'))); + + // ? 没有角色 + wl.push(isNull(pacAuthLinkUserRole.roleId).if(!isExistKey(getPacAuthUserAllDto, 'roleId') && isTrueEnum(getPacAuthUserAllDto.noRole))); + + // ? 没有部门 + wl.push(isNull(pacAuthLinkUserDept.deptId).if(!isExistKey(getPacAuthUserAllDto, 'deptId') && isTrueEnum(getPacAuthUserAllDto.noDept))); + + // ? 没有岗位 + wl.push(isNull(pacAuthLinkUserPost.postId).if(!isExistKey(getPacAuthUserAllDto, 'postId') && isTrueEnum(getPacAuthUserAllDto.noPost))); + + query.where(and(...wl)); + return query; + } + + // DB 查分页 + private async getPage(getPacAuthUserAllDto: GetPacAuthUserAllDto) { + const offset = (getPacAuthUserAllDto.pageNumber - 1) * getPacAuthUserAllDto.pageSize; + + // ! 使用基础查询构建查询总记录数 + const totalCountQuery = this.queryBuilder(getPacAuthUserAllDto, { + totalCount: sql`COUNT(DISTINCT ${pacAuthUser.userId})`, + }); + + // ! 使用基础查询构建分页查询 + // 重命名表 + const userTable1 = alias(pacAuthUser, 'userTable1'); + const userTable2 = alias(pacAuthUser, 'userTable2'); + const selectObject = { + ...this.userPageType, + updateName: userTable1.nickname, + createName: userTable2.nickname, + }; + const paginatedQuery = this.queryBuilder(getPacAuthUserAllDto, { + ...selectObject, + roleList: sql`GROUP_CONCAT(DISTINCT ${pacAuthRole.roleName} ORDER BY ${pacAuthRole.orderNum} DESC SEPARATOR ',')`, + deptList: sql`GROUP_CONCAT(DISTINCT ${pacAuthDept.deptName} ORDER BY ${pacAuthRole.orderNum} DESC SEPARATOR ',')`, + postList: sql`GROUP_CONCAT(DISTINCT ${pacAuthPost.postName} ORDER BY ${pacAuthRole.orderNum} DESC SEPARATOR ',')`, + }) + .leftJoin(userTable2, eq(pacAuthUser.createby, userTable2.userId)) + .leftJoin(userTable1, eq(pacAuthUser.updateby, userTable1.userId)) + .leftJoin(pacAuthRole, eq(pacAuthLinkUserRole.roleId, pacAuthRole.roleId)) + .leftJoin(pacAuthDept, eq(pacAuthLinkUserDept.deptId, pacAuthDept.deptId)) + .leftJoin(pacAuthPost, eq(pacAuthLinkUserPost.postId, pacAuthPost.postId)) + .limit(getPacAuthUserAllDto.pageSize) + .groupBy(pacAuthUser.userId) + .offset(offset); + return { + total: (await totalCountQuery)[0].totalCount, + rowData: await paginatedQuery, + searchData: getPacAuthUserAllDto, + }; + } + + // DB 查列表 + private async getList(getPacAuthUserAllDto: GetPacAuthUserAllDto) { + return this.queryBuilder(getPacAuthUserAllDto, this.userListType); + } + + // DB 查详情 + private async getMore(id: string) { + const userTable1 = alias(pacAuthUser, 'userTable1'); + const userTable2 = alias(pacAuthUser, 'userTable2'); + const user = await this.mysqlService.db + .select({ + ...this.userPageType, + updateName: userTable1.nickname, + createName: userTable2.nickname, + }) + .from(pacAuthUser) + .leftJoin(pacCoreDict, eq(pacAuthUser.userType, pacCoreDict.dictId)) + .leftJoin(userTable2, eq(pacAuthUser.createby, userTable2.userId)) + .leftJoin(userTable1, eq(pacAuthUser.updateby, userTable1.userId)) + .where(and(eq(pacAuthUser.userId, id), isNull(pacAuthUser.deleteby))); + + if (user.length == 0) throw new HttpException('未找到目标用户信息!', HttpStatus.BAD_REQUEST); + const roleList = await this.mysqlService.db + .select({ + roleId: pacAuthRole.roleId, + roleName: pacAuthRole.roleName, + roleKey: pacAuthRole.roleKey, + }) + .from(pacAuthRole) + .leftJoin(pacAuthLinkUserRole, eq(pacAuthLinkUserRole.roleId, pacAuthRole.roleId)) + .where(eq(pacAuthLinkUserRole.userId, id)); + const deptList = await this.mysqlService.db + .select({ + deptId: pacAuthDept.deptId, + deptName: pacAuthDept.deptName, + }) + .from(pacAuthDept) + .leftJoin(pacAuthLinkUserDept, eq(pacAuthLinkUserDept.deptId, pacAuthDept.deptId)) + .where(eq(pacAuthLinkUserDept.userId, id)); + const postList = await this.mysqlService.db + .select({ + postId: pacAuthPost.postId, + postName: pacAuthPost.postName, + postKey: pacAuthPost.postKey, + }) + .from(pacAuthPost) + .leftJoin(pacAuthLinkUserPost, eq(pacAuthLinkUserPost.postId, pacAuthPost.postId)) + .where(eq(pacAuthLinkUserPost.userId, id)); + return { + ...user[0], + roleList, + deptList, + postList, + }; + } + + // DB 删除账户 + private async deleteUser(id: string, userId) { + const user = await this.mysqlService.db + .select({ id: pacAuthUser.userId, pid: pacAuthUser.pid }) + .from(pacAuthUser) + .where(and(isNull(pacAuthUser.deleteby), eq(pacAuthUser.userId, id))); + if (user.length == 0) throw new HttpException('未找到目标用户信息,无法删除!', HttpStatus.BAD_REQUEST); + + // ! 清理角色菜单关联表 + await this.mysqlService.db.delete(pacAuthLinkUserRole).where(eq(pacAuthLinkUserRole.userId as any, id)); + + // ! 清理部门菜单关联表 + await this.mysqlService.db.delete(pacAuthLinkUserDept).where(eq(pacAuthLinkUserDept.userId as any, id)); + + // ! 清理岗位菜单关联表 + await this.mysqlService.db.delete(pacAuthLinkUserPost).where(eq(pacAuthLinkUserPost.userId as any, id)); + + // ! 判断父节点是否存在 + if (user[0].pid != 0) { + // ! 减少父级子节点数量 + await this.haveChildrenSelfIncreasing(user[0].pid, false); + } + + // ! 删除角色数据 + return await this.mysqlService.db + .update(pacAuthUser) + .set({ + deletetime: sql`now()`, + deleteby: userId, + }) + .where(eq(pacAuthUser.userId, id)); + } + + // DB 更新账户 + private async updateUser(id: string, updateAuthUserDto: UpdateAuthUserDto, pacInfo: PacInfoType) { + const user = await this.mysqlService.db + .select({ id: pacAuthUser.userId, pid: pacAuthUser.pid }) + .from(pacAuthUser) + .where(and(isNull(pacAuthUser.deleteby), eq(pacAuthUser.userId, id))); + if (user.length == 0) throw new HttpException('未找到目标用户信息,无法修改!', HttpStatus.BAD_REQUEST); + + // 用户名不修改不需要查重 + + if (updateAuthUserDto.roleList) { + await this.updateUserRole(id, updateAuthUserDto.roleList, pacInfo); + } + if (updateAuthUserDto.deptList) { + await this.updateUserDept(id, updateAuthUserDto.deptList, pacInfo); + } + if (updateAuthUserDto.postList) { + await this.updateUserPost(id, updateAuthUserDto.postList, pacInfo); + } + + return await this.mysqlService.db + .update(pacAuthUser) + .set({ + nickname: updateAuthUserDto.nickname, + userType: updateAuthUserDto.userType, + userEmail: updateAuthUserDto.userEmail, + avatar: updateAuthUserDto.avatar, + userPhone: updateAuthUserDto.userPhone, + userDesc: updateAuthUserDto.userDesc, + status: updateAuthUserDto.status, + updateby: pacInfo.userId, + updatetime: sql`now()`, + }) + .where(eq(pacAuthUser.userId, id)); + } + + // DB 修改密码 + private async resetPassword(id: string, newPassword: string | null) { + let password = ''; + if (newPassword != null) { + password = newPassword; + } else { + // ! 从Redis获取默认密码 + const defaultPasswordForRedis = await this.redisService.get('ENV_DEFAULT_PASSWORD'); + + // ? 判断redis中是否存在默认密码环境变量 + if (!defaultPasswordForRedis) { + password = this.config.get('system.defaultPassword'); + } else { + password = defaultPasswordForRedis; + } + } + + // ! 加密密码 + const pass = cryptoPassword(password, this.config.get('system.passwordSalt')); + + return await this.mysqlService.db + .update(pacAuthUser) + .set({ + password: pass, + }) + .where(eq(pacAuthUser.userId, id)); + } + + // DB 查看目标是否存在 + private async checkUserExist(id: string) { + const user = await this.mysqlService.db + .select({ id: pacAuthUser.userId, pid: pacAuthUser.pid }) + .from(pacAuthUser) + .where(and(isNull(pacAuthUser.deleteby), eq(pacAuthUser.userId, id))); + if (user.length == 0) throw new HttpException('未找到目标用户信息,无法修改!', HttpStatus.BAD_REQUEST); + return user[0]; + } + + // DB 登陆检测 + private async signinDetection(usernameSignInDto: UsernameSignInDto) { + const user = await this.mysqlService.db + .select({ userId: pacAuthUser.userId, pid: pacAuthUser.pid, password: pacAuthUser.password }) + .from(pacAuthUser) + .where(and(isNull(pacAuthUser.deleteby), eq(pacAuthUser.username, usernameSignInDto.username))); + if (user.length == 0) throw new HttpException('该用户不存在!', HttpStatus.BAD_REQUEST); + + // ! 判断是否超过最大登录次数 + const number = await this.redisService.get('SIGNIN_NUM' + user[0].userId); + let maxNumberFieldsigninForRedis = (await this.redisService.get('CONFIG_MAX_NUMBER_FIELD_SIGNIN')) as any; + if (!maxNumberFieldsigninForRedis) { + maxNumberFieldsigninForRedis = this.config.get('system.signin.maxNumberFieldsignin'); + } + if (Number(number) >= maxNumberFieldsigninForRedis) { + throw new HttpException(`登录错误次数过多:${number}次!`, HttpStatus.BAD_REQUEST); + } + + // ! 验证密码 + const pass = cryptoPassword(usernameSignInDto.password, this.config.get('system.passwordSalt')); + if (pass != user[0].password) { + let maxTimeFieldsignin = (await this.redisService.get('CONFIG_MAX_TIME_FIELD_SIGNIN')) as any; + if (!maxTimeFieldsignin) { + maxTimeFieldsignin = this.config.get('system.signin.maxTimeFieldsignin'); + } + await this.redisService.redis.SET('SIGNIN_NUM' + user[0].userId, Number(number) ? Number(number) + 1 : 1, { + PX: maxTimeFieldsignin, + }); + throw new HttpException(`用户名或密码错误:${Number(number) + 1}次!`, HttpStatus.BAD_REQUEST); + } + + // ! 判断是否是最大登录数 + const clientList = await this.redisService.redis.keys('CLIENT-' + user[0].userId + '-*'); + let maxSigninClient = (await this.redisService.get('CONFIG_MAX_SIGNIN_CLIENT')) as any; + if (!maxSigninClient) { + maxSigninClient = this.config.get('system.signin.maxSigninClient'); + } + console.log(maxSigninClient, clientList.length >= maxSigninClient) + if (clientList.length >= maxSigninClient) { + throw new HttpException('已登陆的客户端超过限制!', HttpStatus.BAD_REQUEST); + } + + // ! 签发Token + const token = this.jwt.token({ username: usernameSignInDto.username, userId: user[0].userId, timestamp: new Date().getTime() }); + const refreshToken = this.jwt.refreshToken({ username: usernameSignInDto.username, userId: user[0].userId, timestamp: new Date().getTime() }); + + // 将登陆的账户放进Redis + await this.redisService.redis.set('CLIENT-' + user[0].userId + '-' + MD5(refreshToken), 0, { + EX: this.config.get('system.signin.refreshTokenTime'), + }); + await this.redisService.redis.set('CLIENT-ONLINE-' + user[0].userId + '-' + MD5(token), 0, { + EX: this.config.get('system.signin.refreshTokenTime'), + }); + + return { + token, + refreshToken, + }; + } } diff --git a/src/application/auth-user/dto/create-auth-user.dto.ts b/src/application/auth-user/dto/create-auth-user.dto.ts index 7f571be..de634ed 100644 --- a/src/application/auth-user/dto/create-auth-user.dto.ts +++ b/src/application/auth-user/dto/create-auth-user.dto.ts @@ -1 +1,161 @@ -export class CreateAuthUserDto {} +import { ApiProperty } from '@nestjs/swagger'; +import Trim from '@common/decorator/trim/trim.decorator'; +import { ArrayMaxSize, ArrayMinSize, IsOptional, IsString, Length } from 'class-validator'; +import FormatUsername from "@common/decorator/formatUsername/formatUsername"; + +export class CreateAuthUserDto { + @ApiProperty({ + description: '账户父ID', + type: String, + example: '0', + required: false, + minLength: 1, + maxLength: 32, + }) + @Trim() + @IsString({ message: '部门关联属性应为字符串格式!' }) + @IsOptional() + readonly pid?: string; + + @ApiProperty({ + description: '头像', + type: String, + example: '0', + required: false, + minLength: 1, + maxLength: 255, + }) + @Trim() + @IsString({ message: '头像应为字符串格式!' }) + @Length(1, 255, { message: '请将头像长度控制在1到255位之间!' }) + @IsOptional() + readonly avatar?: string; + + @ApiProperty({ + description: '昵称', + type: String, + example: '研发经理', + required: true, + minLength: 1, + maxLength: 32, + }) + @Trim() + @IsString({ message: '昵称应为字符串格式!' }) + @Length(1, 32, { message: '昵称长度控制在1到32位之间!' }) + readonly nickname: string; + + @ApiProperty({ + description: '岗位Id列表', + type: [String], + example: ['a'], + required: true, + minItems: 0, + maxItems: 100, + }) + @IsString({ each: true, message: '岗位Id格式错误' }) + @ArrayMinSize(0, { message: '至少需要选择一个岗位' }) + @ArrayMaxSize(100, { message: '需要绑定的岗位超过限制' }) + @Length(19, 19, { each: true, message: '岗位Id格式错误' }) + @IsOptional() + readonly postList: string[]; + + @ApiProperty({ + description: '角色Id列表', + type: [String], + example: ['a'], + required: true, + minItems: 0, + maxItems: 100, + }) + @IsString({ each: true, message: '角色Id格式错误' }) + @ArrayMinSize(0, { message: '至少需要选择一个角色' }) + @ArrayMaxSize(100, { message: '需要绑定的角色超过限制' }) + @Length(19, 19, { each: true, message: '角色Id格式错误' }) + @IsOptional() + readonly roleList?: string[]; + + @ApiProperty({ + description: '部门Id列表', + type: [String], + example: ['a'], + required: true, + minItems: 0, + maxItems: 100, + }) + @IsString({ each: true, message: '部门Id格式错误' }) + @ArrayMinSize(0, { message: '至少需要选择一个部门' }) + @ArrayMaxSize(100, { message: '需要绑定的部门超过限制' }) + @Length(19, 19, { each: true, message: '部门Id格式错误' }) + @IsOptional() + readonly deptList: string[]; + + @ApiProperty({ + description: '账户描述', + type: String, + example: '0', + required: false, + minLength: 1, + maxLength: 255, + }) + @Trim() + @IsString({ message: '账户描述应为字符串格式!' }) + @Length(1, 255, { message: '请将账户描述长度控制在1到255位之间!' }) + @IsOptional() + readonly userDesc: string; + + @ApiProperty({ + description: '邮箱', + type: String, + example: '0', + required: false, + minLength: 5, + maxLength: 255, + }) + @Trim() + @IsString({ message: '邮箱应为字符串格式!' }) + @Length(5, 255, { message: '请将邮箱长度控制在5到255位之间!' }) + @IsOptional() + readonly userEmail: string; + + @ApiProperty({ + description: '用户名', + type: String, + example: '0', + required: false, + minLength: 4, + maxLength: 128, + }) + @Trim() + @FormatUsername() + @IsString({ message: '用户名应为字符串格式!' }) + @Length(4, 128, { message: '请将用户名长度控制在4到128位之间!' }) + readonly username: string; + + @ApiProperty({ + description: '手机号', + type: String, + example: '0', + required: false, + minLength: 11, + maxLength: 11, + }) + @Trim() + @IsString({ message: '手机号格式不正确!' }) + @Length(11, 11, { message: '手机号格式不正确!' }) + @IsOptional() + readonly userPhone: string; + + @ApiProperty({ + description: '账户类型,来自于字典', + type: String, + example: '0', + required: false, + minLength: 19, + maxLength: 19, + }) + @Trim() + @IsString({ message: '账户类型格式不正确!' }) + @Length(19, 19, { message: '账户类型格式不正确!' }) + @IsOptional() + readonly userType: string; +} diff --git a/src/application/auth-user/dto/get-auth-user.dto.ts b/src/application/auth-user/dto/get-auth-user.dto.ts new file mode 100644 index 0000000..d50d709 --- /dev/null +++ b/src/application/auth-user/dto/get-auth-user.dto.ts @@ -0,0 +1,175 @@ +// | ------------------------------------------------------------ +// | @版本: version 0.1 +// | @创建人: 【Nie-x7129】 +// | @E-mail: x71291@outlook.com +// | @所在项目: pac-auth +// | @文件描述: get-auth-role.dto.ts - +// | @创建时间: 2024-06-25 17:22 +// | @更新时间: 2024-06-25 17:22 +// | @修改记录: +// | -*-*-*- (时间--修改人--修改说明) -*-*-*- +// | = +// | ------------------------------------------------------------ + +import { GetDto } from '@dto/get.dto'; +import { ApiProperty } from '@nestjs/swagger'; +import Trim from '@common/decorator/trim/trim.decorator'; +import { ArrayMaxSize, ArrayMinSize, IsEnum, IsInt, IsOptional, IsString, Length, Max, Min } from 'class-validator'; +import { BooleanEnum } from '@utils/boolean.enum'; +import Int from '@common/decorator/int/int.descrator'; + +export class GetPacAuthUserAllDto extends GetDto { + @ApiProperty({ + description: '账户信息', + type: String, + example: '管理员', + required: false, + minLength: 1, + maxLength: 128, + }) + @Trim() + @IsString({ message: '账户信息应为字符串格式!' }) + @Length(0, 128, { message: '请将账户信息长度控制在1到128位之间!' }) + @IsOptional() + readonly userInfo?: string; + + @ApiProperty({ + description: '账户类型,来自于字典', + type: String, + example: '0', + required: false, + minLength: 19, + maxLength: 19, + }) + @Trim() + @IsString({ message: '账户类型格式不正确!' }) + @Length(19, 19, { message: '账户类型格式不正确!' }) + @IsOptional() + readonly userType: string; + + @ApiProperty({ + description: '是否存在子账户', + type: BooleanEnum, + enum: BooleanEnum, + example: 0, + required: false, + }) + @Trim() + @IsEnum(BooleanEnum, { message: 'haveChildren参数格式错误' }) + @IsOptional() + readonly haveChildren: BooleanEnum; + + @ApiProperty({ + description: '账户状态', + type: Number, + example: 0, + required: false, + minimum: -100, + maximum: 100, + }) + @Trim() + @Int() + @IsInt({ + message: '账户状态必须是整数!', + }) + @Min(-100, { + message: '账户状态需要大于-100!', + }) + @Max(100, { + message: '账户状态不能超过100', + }) + @IsOptional() + readonly status?: string; + + @ApiProperty({ + description: '账户层级id', + type: Number, + example: 0, + required: false, + minimum: 0, + maximum: 100, + }) + @Trim() + @IsString({ message: '账户层级id应为字符串格式!' }) + @Length(1, 20, { message: '账户层级id格式错误!' }) + @IsOptional() + readonly hierarchy?: string; + + @ApiProperty({ + description: '角色Id', + type: String, + example: '0', + required: false, + minLength: 19, + maxLength: 19, + }) + @Trim() + @IsString({ message: '角色Id类型格式不正确!' }) + @Length(19, 19, { message: '角色Id类型格式不正确!' }) + @IsOptional() + readonly roleId?: string; + + @ApiProperty({ + description: '部门Id', + type: String, + example: '0', + required: false, + minLength: 19, + maxLength: 19, + }) + @Trim() + @IsString({ message: '部门Id类型格式不正确!' }) + @Length(19, 19, { message: '部门Id类型格式不正确!' }) + @IsOptional() + readonly deptId?: string; + + @ApiProperty({ + description: '岗位Id', + type: String, + example: '0', + required: false, + minLength: 19, + maxLength: 19, + }) + @Trim() + @IsString({ message: '岗位Id类型格式不正确!' }) + @Length(19, 19, { message: '岗位Id类型格式不正确!' }) + @IsOptional() + readonly postId?: string; + + @ApiProperty({ + description: '查没有角色的账户', + type: BooleanEnum, + enum: BooleanEnum, + example: 0, + required: false, + }) + @Trim() + @IsEnum(BooleanEnum, { message: 'noRole参数格式错误' }) + @IsOptional() + readonly noRole: BooleanEnum; + + @ApiProperty({ + description: '查没有部门的账户', + type: BooleanEnum, + enum: BooleanEnum, + example: 0, + required: false, + }) + @Trim() + @IsEnum(BooleanEnum, { message: 'noDept参数格式错误' }) + @IsOptional() + readonly noDept: BooleanEnum; + + @ApiProperty({ + description: '查没有岗位的账户', + type: BooleanEnum, + enum: BooleanEnum, + example: 0, + required: false, + }) + @Trim() + @IsEnum(BooleanEnum, { message: 'noPost参数格式错误' }) + @IsOptional() + readonly noPost: BooleanEnum; +} diff --git a/src/application/auth-user/dto/password.dto.ts b/src/application/auth-user/dto/password.dto.ts new file mode 100644 index 0000000..2a36016 --- /dev/null +++ b/src/application/auth-user/dto/password.dto.ts @@ -0,0 +1,48 @@ +// | ------------------------------------------------------------ +// | @版本: version 0.1 +// | @创建人: 【Nie-hotok】 +// | @E-mail: x71291@outlook.com +// | @所在项目: pac-auth +// | @文件描述: password.dto.ts - +// | @创建时间: 2024-06-29 22:01 +// | @更新时间: 2024-06-29 22:01 +// | @修改记录: +// | -*-*-*- (时间--修改人--修改说明) -*-*-*- +// | = +// | ------------------------------------------------------------ + +import { ApiProperty } from '@nestjs/swagger'; +import Trim from '@common/decorator/trim/trim.decorator'; +import FormatUsername from '@common/decorator/formatUsername/formatUsername'; +import { IsString, Length } from 'class-validator'; + +export class PasswordDto { + @ApiProperty({ + description: '新密码', + type: String, + example: '0', + required: false, + minLength: 8, + maxLength: 128, + }) + @Trim() + @FormatUsername() + @IsString({ message: '新密码应为字符串格式!' }) + @Length(8, 128, { message: '请将新密码长度控制在8到128位之间!' }) + readonly newPassword: string; + + + @ApiProperty({ + description: '旧密码', + type: String, + example: '0', + required: false, + minLength: 6, + maxLength: 128, + }) + @Trim() + @FormatUsername() + @IsString({ message: '旧密码应为字符串格式!' }) + @Length(6, 128, { message: '请将旧密码长度控制在6到128位之间!' }) + readonly oldPassword: string; +} diff --git a/src/application/auth-user/dto/signin.dto.ts b/src/application/auth-user/dto/signin.dto.ts new file mode 100644 index 0000000..ccb378a --- /dev/null +++ b/src/application/auth-user/dto/signin.dto.ts @@ -0,0 +1,46 @@ +// | ------------------------------------------------------------ +// | @版本: version 0.1 +// | @创建人: 【Nie-hotok】 +// | @E-mail: x71291@outlook.com +// | @所在项目: pac-auth +// | @文件描述: signin.dto.ts - +// | @创建时间: 2024-06-29 22:30 +// | @更新时间: 2024-06-29 22:30 +// | @修改记录: +// | -*-*-*- (时间--修改人--修改说明) -*-*-*- +// | = +// | ------------------------------------------------------------ +import { ApiProperty } from '@nestjs/swagger'; +import Trim from '@common/decorator/trim/trim.decorator'; +import FormatUsername from '@common/decorator/formatUsername/formatUsername'; +import { IsString, Length } from 'class-validator'; + +export class UsernameSignInDto { + @ApiProperty({ + description: '用户名', + type: String, + example: '0', + required: false, + minLength: 4, + maxLength: 128, + }) + @Trim() + @FormatUsername() + @IsString({ message: '用户名应为字符串格式!' }) + @Length(4, 128, { message: '请将用户名长度控制在4到128位之间!' }) + readonly username: string; + + @ApiProperty({ + description: '新密码', + type: String, + example: '0', + required: false, + minLength: 6, + maxLength: 128, + }) + @Trim() + @FormatUsername() + @IsString({ message: '新密码应为字符串格式!' }) + @Length(6, 128, { message: '请将新密码长度控制在6到128位之间!' }) + readonly password: string; +} diff --git a/src/application/auth-user/dto/update-auth-user.dto.ts b/src/application/auth-user/dto/update-auth-user.dto.ts index 400cb5d..d259dd7 100644 --- a/src/application/auth-user/dto/update-auth-user.dto.ts +++ b/src/application/auth-user/dto/update-auth-user.dto.ts @@ -1,4 +1,25 @@ -import { PartialType } from '@nestjs/swagger'; +import { ApiProperty, PartialType } from '@nestjs/swagger'; import { CreateAuthUserDto } from './create-auth-user.dto'; +import { IsInt, IsOptional, Max, Min } from 'class-validator'; -export class UpdateAuthUserDto extends PartialType(CreateAuthUserDto) {} +export class UpdateAuthUserDto extends PartialType(CreateAuthUserDto) { + @ApiProperty({ + description: '状态', + type: Number, + example: 10, + required: false, + minimum: -100, + maximum: 100, + }) + @IsOptional() + @IsInt({ + message: '状态必须是整数!', + }) + @Min(-1000, { + message: '状态不能小于-100!', + }) + @Max(1000, { + message: '状态不能超过100', + }) + readonly status: number; +} diff --git a/src/application/auth-user/dto/updateLink.dto.ts b/src/application/auth-user/dto/updateLink.dto.ts new file mode 100644 index 0000000..f0d9500 --- /dev/null +++ b/src/application/auth-user/dto/updateLink.dto.ts @@ -0,0 +1,31 @@ +// | ------------------------------------------------------------ +// | @版本: version 0.1 +// | @创建人: 【Nie-hotok】 +// | @E-mail: x71291@outlook.com +// | @所在项目: pac-auth +// | @文件描述: updateLink.dto.ts - +// | @创建时间: 2024-06-29 22:07 +// | @更新时间: 2024-06-29 22:07 +// | @修改记录: +// | -*-*-*- (时间--修改人--修改说明) -*-*-*- +// | = +// | ------------------------------------------------------------ + +import { ApiProperty } from '@nestjs/swagger'; +import { ArrayMaxSize, ArrayMinSize, IsString, Length } from 'class-validator'; + +export class UpdateLinkDto { + @ApiProperty({ + description: 'Id列表', + type: [String], + example: ['a'], + required: true, + minItems: 0, + maxItems: 100, + }) + @IsString({ each: true, message: 'Id格式错误' }) + @ArrayMinSize(0, { message: '至少需要选择一个id' }) + @ArrayMaxSize(100, { message: '需要绑定的id超过限制' }) + @Length(19, 19, { each: true, message: 'Id格式错误' }) + readonly list: string[]; +} diff --git a/src/common/decorator/formatUsername/formatUsername.ts b/src/common/decorator/formatUsername/formatUsername.ts new file mode 100644 index 0000000..e0ec04d --- /dev/null +++ b/src/common/decorator/formatUsername/formatUsername.ts @@ -0,0 +1,26 @@ +// | ------------------------------------------------------------ +// | @版本: version 0.1 +// | @创建人: 【Nie-hotok】 +// | @E-mail: x71291@outlook.com +// | @所在项目: pac-auth +// | @文件描述: formatUsername.ts - +// | @创建时间: 2024-06-29 16:20 +// | @更新时间: 2024-06-29 16:20 +// | @修改记录: +// | -*-*-*- (时间--修改人--修改说明) -*-*-*- +// | = +// | ------------------------------------------------------------ + +import { Transform } from 'class-transformer'; + +export default function FormatUsername() { + return Transform(({ value }) => { + if (typeof value === 'string') { + if (value.trim() == '') { + return undefined; + } + return value.replace(/\s/g, '').toLowerCase(); + } + return value; + }); +} diff --git a/src/common/service/jwt/jwt.service.ts b/src/common/service/jwt/jwt.service.ts new file mode 100644 index 0000000..b18be04 --- /dev/null +++ b/src/common/service/jwt/jwt.service.ts @@ -0,0 +1,28 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { LoggerService } from '@service/logger/logger.service'; +import * as jwt from 'jsonwebtoken'; + +@Injectable() +export class JwtService { + constructor( + private readonly configService: ConfigService, + private readonly logger: LoggerService, + ) {} + + public token(payload: any): Promise { + return jwt.sign(payload, this.configService.get('system.signin.secretKey'), { + expiresIn: this.configService.get('system.signin.tokenTime'), + }); + } + + public refreshToken(payload: any) { + return jwt.sign(payload, this.configService.get('system.signin.secretKey'), { + expiresIn: this.configService.get('system.signin.refreshTokenTime'), + }); + } + + public verify(token: any) { + jwt.verify(token, this.configService.get('system.signin.secretKey')); + } +} diff --git a/src/common/service/mysql/mysql.service.spec.ts b/src/common/service/mysql/mysql.service.spec.ts deleted file mode 100644 index e5bdaa8..0000000 --- a/src/common/service/mysql/mysql.service.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { MysqlService } from './mysql.service'; - -describe('MysqlService', () => { - let service: MysqlService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [MysqlService], - }).compile(); - - service = module.get(MysqlService); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); -}); diff --git a/src/common/service/redis/redis.service.spec.ts b/src/common/service/redis/redis.service.spec.ts deleted file mode 100644 index b765f1c..0000000 --- a/src/common/service/redis/redis.service.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -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/src/common/service/redis/redis.service.ts b/src/common/service/redis/redis.service.ts index d937b88..9b4c28f 100644 --- a/src/common/service/redis/redis.service.ts +++ b/src/common/service/redis/redis.service.ts @@ -5,7 +5,7 @@ import { LoggerService } from '@service/logger/logger.service'; @Injectable() export class RedisService { - private readonly redis: RedisClientType; + public readonly redis: RedisClientType; constructor( private readonly configService: ConfigService, private readonly logger: LoggerService, diff --git a/src/config/configuration.ts b/src/config/configuration.ts index 8c589a7..56ef0cd 100644 --- a/src/config/configuration.ts +++ b/src/config/configuration.ts @@ -14,6 +14,8 @@ export default () => ({ allEnv: process.env, env: process.env['NODE_ENV'] || 'development', + + // 最高管理员ID masterId: 0, main: { host: '0.0.0.0' || 'localhost', @@ -23,6 +25,7 @@ export default () => ({ version: process.env.npm_package_version || '0.0.1', }, logger: { + // 日志记录等级 level: process.env['NODE_ENV'] === undefined || 'development' ? 'trace' : 'warning', }, swagger: { @@ -45,4 +48,31 @@ export default () => ({ password: 'Hxl1314521', }, }, + system: { + // 默认密码 + defaultPassword: '123456', + + // 密码盐 + passwordSalt: '6dea8337e824f71ab960ea896c56f9ea', + + signin: { + // 最大错误登录次数 + maxNumberFieldsignin: 5, + + // 登陆错误等待时间 10min + maxTimeFieldsignin: 1000 * 60 * 10, + + // 最大登录客户端数 + maxSigninClient: 5, + + // 客户端在线时间 7天 ========jwt最小单位是s + refreshTokenTime: 60 * 60 * 24 * 7, + + // 客户端连续在线时间 10min + tokenTime: 60 * 10, + + // token盐 + secretKey: '326dea8337e8xsxa24f71ab960ea', + }, + }, }); diff --git a/src/dto/AttLinkUser.dto.ts b/src/dto/AttLinkUser.dto.ts index 5b98ebc..4b15eb8 100644 --- a/src/dto/AttLinkUser.dto.ts +++ b/src/dto/AttLinkUser.dto.ts @@ -38,12 +38,11 @@ export class RoleLinkUserDto { }) @IsString({ each: true, message: '账户Id格式错误' }) @ArrayMinSize(1, { message: '至少需要选择一个需要绑定的账户' }) - @ArrayMaxSize(200, { message: '需要绑定的账户账户超过限制' }) + @ArrayMaxSize(200, { message: '需要绑定的账户超过限制' }) @Length(19, 19, { each: true, message: '账户Id格式错误' }) readonly userIdList?: string[]; } - export class DeptLinkUserDto { @ApiProperty({ description: '角色ID', diff --git a/src/entities/relations.ts b/src/entities/relations.ts index 80768e2..34c5368 100644 --- a/src/entities/relations.ts +++ b/src/entities/relations.ts @@ -1,3 +1,2 @@ -import { relations } from "drizzle-orm/relations"; -import { } from "./schema"; - +import { relations } from 'drizzle-orm/relations'; +import {} from './schema'; diff --git a/src/utils/MD5.ts b/src/utils/MD5.ts new file mode 100644 index 0000000..8af1c0b --- /dev/null +++ b/src/utils/MD5.ts @@ -0,0 +1,17 @@ +// | ------------------------------------------------------------ +// | @版本: version 0.1 +// | @创建人: 【Nie-hotok】 +// | @E-mail: x71291@outlook.com +// | @所在项目: pac-auth +// | @文件描述: MD5.ts - +// | @创建时间: 2024-06-29 23:50 +// | @更新时间: 2024-06-29 23:50 +// | @修改记录: +// | -*-*-*- (时间--修改人--修改说明) -*-*-*- +// | = +// | ------------------------------------------------------------ +import * as crypto from 'node:crypto'; + +export default function MD5(data) { + return crypto.createHash('md5').update(data).digest('hex').toUpperCase(); +} diff --git a/src/utils/cryptoPassword.ts b/src/utils/cryptoPassword.ts new file mode 100644 index 0000000..90e6713 --- /dev/null +++ b/src/utils/cryptoPassword.ts @@ -0,0 +1,20 @@ +// | ------------------------------------------------------------ +// | @版本: version 0.1 +// | @创建人: 【Nie-hotok】 +// | @E-mail: x71291@outlook.com +// | @所在项目: pac-auth +// | @文件描述: cryptoPassword.ts - +// | @创建时间: 2024-06-29 15:44 +// | @更新时间: 2024-06-29 15:44 +// | @修改记录: +// | -*-*-*- (时间--修改人--修改说明) -*-*-*- +// | = +// | ------------------------------------------------------------ + +import * as crypto from 'node:crypto'; +export default function cryptoPassword(str: string, salt) { + return crypto + .createHash('sha256') + .update(salt + str) + .digest('hex'); +} diff --git a/test/密码随机盐.js b/test/密码随机盐.js new file mode 100644 index 0000000..37afe5a --- /dev/null +++ b/test/密码随机盐.js @@ -0,0 +1,16 @@ +// | ------------------------------------------------------------ +// | @版本: version 0.1 +// | @创建人: 【Nie-hotok】 +// | @E-mail: x71291@outlook.com +// | @所在项目: pac-auth +// | @文件描述: 密码随机盐.js - +// | @创建时间: 2024-06-29 15:46 +// | @更新时间: 2024-06-29 15:46 +// | @修改记录: +// | -*-*-*- (时间--修改人--修改说明) -*-*-*- +// | = +// | ------------------------------------------------------------ +const crypto = require('crypto'); +const salt = crypto.randomBytes(16).toString('hex'); + +console.log('A', salt);