feat: 调整项目层级

This commit is contained in:
2024-11-27 14:30:20 +08:00
parent 5eb78c35c2
commit b02e2d2160
43 changed files with 17 additions and 12 deletions

View File

@@ -0,0 +1,30 @@
import { Controller, Post, Body, Param } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthDto } from './dto/auth.dto';
import { SkipAuth } from '@/common/decorators/skip-auth.decorator';
import { UserRoleDto } from './dto/user-role.dto';
import { RolePermissionDto } from './dto/role-permission.dto';
@Controller()
export class AuthController {
constructor(private readonly authService: AuthService) {}
// 用户登录
@Post('login')
@SkipAuth()
signIn(@Body() authDto: AuthDto) {
return this.authService.signIn(authDto);
}
// 授权用户角色
@Post('userRole/:id')
grantUserRole(@Param('id') id: string, @Body() dto: UserRoleDto) {
return this.authService.grantUserRole(+id, dto);
}
// 授权角色权限
@Post('rolePermission/:id')
grantRolePermission(@Param('id') id: string, @Body() dto: RolePermissionDto) {
return this.authService.grantRolePermission(+id, dto);
}
}

View File

@@ -0,0 +1,26 @@
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { UserModule } from '../user/user.module';
import { JwtModule } from '@nestjs/jwt';
import { ConfigModule, ConfigService } from '@nestjs/config';
@Module({
imports: [
UserModule,
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
global: true,
useFactory: async (config: ConfigService) => {
return {
secret: config.get('jwt.secret'),
signOptions: { expiresIn: config.get('jwt.expiresIn') },
};
},
}),
],
controllers: [AuthController],
providers: [AuthService],
})
export class AuthModule {}

View File

@@ -0,0 +1,136 @@
import {
ForbiddenException,
HttpException,
HttpStatus,
Inject,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { AuthDto } from './dto/auth.dto';
import * as bcrypt from 'bcryptjs';
import { PrismaService } from '@/prisma/prisma.service';
import { UserRoleDto } from './dto/user-role.dto';
import { RolePermissionDto } from './dto/role-permission.dto';
import { RedisGroup } from '@/common/enum';
import { ConfigService } from '@nestjs/config';
import { Cache, CACHE_MANAGER } from '@nestjs/cache-manager';
import { nanoid } from 'nanoid';
@Injectable()
export class AuthService {
constructor(
private readonly jwtService: JwtService,
private readonly prisma: PrismaService,
private readonly configService: ConfigService,
@Inject(CACHE_MANAGER) private cacheService: Cache,
) {}
// 登录
async signIn({ account, password }: AuthDto) {
const user = await this.prisma.sysUser.findUnique({
where: {
account,
},
});
if (!user) {
throw new UnauthorizedException('账号或密码错误!');
}
const pass = await bcrypt.hash(password, user.passwordSalt);
if (user?.password !== pass) {
throw new UnauthorizedException('账号或密码错误!');
}
if (user.status === 0) {
throw new ForbiddenException('此账户已被禁用!');
}
const jti = nanoid();
const payload = { sub: user.id, account: user.account, jti };
const accessToken = await this.jwtService.signAsync(payload);
await this.cacheService.set(
`${RedisGroup.JwtJti}:${user.id}:${jti}`,
user.id,
this.configService.get('jwt.redisExpiresIn'),
);
return { accessToken };
}
async grantUserRole(id: number, { roleIds }: UserRoleDto) {
const user = await this.prisma.sysUser.findUnique({
where: { id },
});
if (!user) throw new HttpException('用户不存在!', HttpStatus.NOT_FOUND);
// 查询所有角色id
const allRoles = await this.prisma.sysRole.findMany({
select: { id: true },
});
const allRoleIds = allRoles.map((item) => item.id);
const isContain = roleIds.every((item) => allRoleIds.includes(item));
if (!isContain)
throw new HttpException(
'出现不存在的角色id,请检查传入的roleIds',
HttpStatus.BAD_REQUEST,
);
const data = roleIds.map((item) => ({
userId: id,
roleId: item,
}));
await this.prisma.$transaction(async (prisma) => {
await prisma.sysUserRole.deleteMany({
where: { userId: id },
});
await prisma.sysUserRole.createMany({ data });
});
}
async grantRolePermission(id: number, { permissionIds }: RolePermissionDto) {
const role = await this.prisma.sysRole.findUnique({
where: { id },
});
if (!role)
throw new HttpException('角色信息不存在!', HttpStatus.NOT_FOUND);
// 查询所有权限
const allPermission = await this.prisma.sysPermission.findMany({
select: { id: true },
});
const allPermissionIds = allPermission.map((item) => item.id);
// 判断传入的id是否都存在于数据库中
const isContain = permissionIds.every((item) =>
allPermissionIds.includes(item),
);
if (!isContain)
throw new HttpException(
'出现不存在的权限id,请检查传入的permissionIds',
HttpStatus.BAD_REQUEST,
);
const data = permissionIds.map((permissionId) => ({
roleId: id,
permissionId,
}));
await this.prisma.$transaction([
this.prisma.sysRolePermission.deleteMany({
where: { permissionId: { in: permissionIds } },
}),
this.prisma.sysRolePermission.createMany({ data }),
]);
}
}

View File

@@ -0,0 +1,7 @@
import { CreateUserDto } from '@/modules/system/user/dto/create-user.dto';
import { PickType } from '@nestjs/mapped-types';
export class AuthDto extends PickType(CreateUserDto, [
'account',
'password',
] as const) {}

View File

@@ -0,0 +1,8 @@
import { ArrayUnique, IsInt, IsNotEmpty } from 'class-validator';
export class RolePermissionDto {
@IsNotEmpty()
@IsInt({ each: true })
@ArrayUnique()
permissionIds: number[];
}

View File

@@ -0,0 +1,8 @@
import { ArrayUnique, IsInt, IsNotEmpty } from 'class-validator';
export class UserRoleDto {
@IsNotEmpty()
@IsInt({ each: true })
@ArrayUnique()
roleIds: number[];
}

View File

@@ -0,0 +1,22 @@
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Role } from '@/common/enum';
import { ROLES_KEY } from '@/common/decorators/roles.decorator';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);
if (!requiredRoles) {
return true;
}
const { user } = context.switchToHttp().getRequest();
return requiredRoles.some((role) => user.roles?.includes(role));
}
}

View File

@@ -0,0 +1,49 @@
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
Query,
} from '@nestjs/common';
import { ConfigService } from './config.service';
import { CreateConfigDto } from './dto/create-config.dto';
import { UpdateConfigDto } from './dto/update-config.dto';
import { QueryListDto } from './dto/query-list.dto';
@Controller()
export class ConfigController {
constructor(private readonly configService: ConfigService) {}
@Post()
create(@Body() dto: CreateConfigDto) {
return this.configService.create(dto);
}
@Get()
findAll(@Query() dto: QueryListDto) {
return this.configService.findAll(dto);
}
@Get(':key')
findOne(@Param('key') key: string) {
return this.configService.findOne(key);
}
@Patch(':key')
update(@Param('key') key: string, @Body() updateConfigDto: UpdateConfigDto) {
return this.configService.update(key, updateConfigDto);
}
@Delete(':key')
remove(@Param('key') key: string) {
return this.configService.remove(key);
}
@Post('sync')
syncConfig() {
return this.configService.syncConfig();
}
}

View File

@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { ConfigService } from './config.service';
import { ConfigController } from './config.controller';
@Module({
controllers: [ConfigController],
providers: [ConfigService],
})
export class ConfigModule {}

View File

@@ -0,0 +1,104 @@
import { Injectable, HttpException, HttpStatus, Inject } from '@nestjs/common';
import { CreateConfigDto } from './dto/create-config.dto';
import { UpdateConfigDto } from './dto/update-config.dto';
import { PrismaService } from '@/prisma/prisma.service';
import { QueryListDto } from './dto/query-list.dto';
import { Prisma } from '@prisma/client';
import { Cache, CACHE_MANAGER } from '@nestjs/cache-manager';
import { RedisGroup } from '@/common/enum';
@Injectable()
export class ConfigService {
constructor(
private readonly prisma: PrismaService,
@Inject(CACHE_MANAGER) private cacheService: Cache,
) {}
findOneByUnique(key: string) {
return this.prisma.sysConfig.findUnique({
where: {
key,
},
});
}
async create(data: CreateConfigDto) {
const doesExists = await this.findOneByUnique(data.key);
if (doesExists) {
throw new HttpException('key值不可重复', HttpStatus.BAD_REQUEST);
}
await this.prisma.sysConfig.create({ data });
}
async findAll(dto: QueryListDto): Promise<QueryListResult> {
const { page, pageSize, key, value } = dto;
const query: Prisma.SysConfigFindManyArgs = {
select: {
key: true,
value: true,
remark: true,
},
skip: pageSize * (page - 1),
take: pageSize,
where: {
...(key && { key: { contains: key } }),
...(value && { value: { contains: value } }),
},
};
const [list, count] = await this.prisma.$transaction([
this.prisma.sysConfig.findMany(query),
this.prisma.sysConfig.count({ where: query.where }),
]);
return {
page,
pageSize,
count,
list,
};
}
findOne(key: string) {
return this.findOneByUnique(key);
}
async update(key: string, updateConfigDto: UpdateConfigDto) {
const doesExists = await this.findOneByUnique(key);
if (!doesExists) {
throw new HttpException('修改失败key值不存在', HttpStatus.NOT_FOUND);
}
await this.prisma.sysConfig.update({
data: updateConfigDto,
where: { key },
});
}
async remove(key: string) {
const doesExists = await this.findOneByUnique(key);
if (!doesExists) {
throw new HttpException('删除失败key值不存在', HttpStatus.NOT_FOUND);
}
await this.prisma.sysConfig.delete({ where: { key } });
}
async syncConfig() {
const allConfigs = await this.prisma.sysConfig.findMany();
const keys = await this.cacheService.store.keys(`${RedisGroup.Config}:*`);
if (keys.length) await this.cacheService.store.mdel(...keys);
const config: [string, unknown][] = allConfigs.map((item) => [
item.key,
item.value,
]);
if (config.length) await this.cacheService.store.mset(config, 0);
}
}

View File

@@ -0,0 +1,14 @@
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
export class CreateConfigDto {
@IsNotEmpty()
@IsString()
key: string;
@IsNotEmpty()
@IsString()
value: string;
@IsOptional()
remark: string;
}

View File

@@ -0,0 +1,10 @@
import { PaginationDto } from '@/common/dto/pagination.dto';
import { IsOptional } from 'class-validator';
export class QueryListDto extends PaginationDto {
@IsOptional()
key?: string;
@IsOptional()
value?: string;
}

View File

@@ -0,0 +1,6 @@
import { OmitType, PartialType } from '@nestjs/mapped-types';
import { CreateConfigDto } from './create-config.dto';
export class UpdateConfigDto extends PartialType(
OmitType(CreateConfigDto, ['key'] as const),
) {}

View File

@@ -0,0 +1,23 @@
import {
Controller,
Post,
UseInterceptors,
UploadedFile,
ParseFilePipe,
} from '@nestjs/common';
import { FilesService } from './files.service';
import { FileInterceptor } from '@nestjs/platform-express';
@Controller()
export class FilesController {
constructor(private readonly filesService: FilesService) {}
@Post()
@UseInterceptors(FileInterceptor('file'))
upload(
@UploadedFile(new ParseFilePipe({ validators: [] }))
file: Express.Multer.File,
) {
return this.filesService.upload(file);
}
}

View File

@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { FilesService } from './files.service';
import { FilesController } from './files.controller';
import { MinioService } from '@/minio/minio.service';
@Module({
controllers: [FilesController],
providers: [FilesService, MinioService],
})
export class FilesModule {}

View File

@@ -0,0 +1,31 @@
import { Inject, Injectable } from '@nestjs/common';
import { PrismaService } from '@/prisma/prisma.service';
import path from 'path';
import { Cache, CACHE_MANAGER } from '@nestjs/cache-manager';
import { MinioService } from '@/minio/minio.service';
import { nanoid } from 'nanoid';
@Injectable()
export class FilesService {
constructor(
private readonly prisma: PrismaService,
@Inject(CACHE_MANAGER) private cacheService: Cache,
private readonly minioService: MinioService,
) {}
async upload(file: Express.Multer.File) {
const fileName = `${nanoid()}${path.extname(file.originalname)}`;
const filePath = await this.minioService.uploadFile(file, fileName);
const res = await this.prisma.files.create({
data: { name: fileName, path: filePath },
});
return {
id: res.id,
path: res.path,
};
}
}

View File

@@ -0,0 +1,19 @@
import { IsInt, IsNotEmpty, IsOptional, IsString } from 'class-validator';
export class CreateMenuDto {
@IsNotEmpty()
@IsString()
name: string;
@IsNotEmpty()
@IsString()
url: string;
@IsOptional()
@IsInt()
parentId?: number;
@IsOptional()
@IsInt()
sort?: number;
}

View File

@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/mapped-types';
import { CreateMenuDto } from './create-menu.dto';
export class UpdateMenuDto extends PartialType(CreateMenuDto) {}

View File

@@ -0,0 +1,42 @@
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
} from '@nestjs/common';
import { MenuService } from './menu.service';
import { CreateMenuDto } from './dto/create-menu.dto';
import { UpdateMenuDto } from './dto/update-menu.dto';
@Controller()
export class MenuController {
constructor(private readonly menuService: MenuService) {}
@Post()
create(@Body() dto: CreateMenuDto) {
return this.menuService.create(dto);
}
@Get('tree')
getTreeMenu() {
return this.menuService.getTreeMenu();
}
@Get()
findAll() {
return this.menuService.findAll();
}
@Patch(':id')
update(@Param('id') id: string, @Body() updateMenuDto: UpdateMenuDto) {
return this.menuService.update(+id, updateMenuDto);
}
@Delete(':id')
remove(@Param('id') id: number) {
return this.menuService.remove(id);
}
}

View File

@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { MenuService } from './menu.service';
import { MenuController } from './menu.controller';
@Module({
controllers: [MenuController],
providers: [MenuService],
})
export class MenuModule {}

View File

@@ -0,0 +1,121 @@
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { CreateMenuDto } from './dto/create-menu.dto';
import { UpdateMenuDto } from './dto/update-menu.dto';
import { PrismaService } from '@/prisma/prisma.service';
import { Prisma } from '@prisma/client';
import { PermissionType } from '@/common/enum';
@Injectable()
export class MenuService {
constructor(private readonly prisma: PrismaService) {}
findOne(params: { id?: number; excludeId?: number }) {
const { id, excludeId } = params;
return this.prisma.sysMenu.findFirst({
where: {
...(id ? { id } : {}),
...(excludeId ? { NOT: { id: excludeId } } : {}),
},
});
}
findOneById(id: number) {
return this.prisma.sysMenu.findUnique({ where: { id } });
}
async create(data: CreateMenuDto) {
await this.prisma.$transaction(async (prisma) => {
const { id: menuId } = await prisma.sysMenu.create({ data });
// 创建菜单权限
const { id: permissionId } = await prisma.sysPermission.create({
data: { type: PermissionType.Menu },
});
// 关联权限和菜单
await prisma.sysMenuPermission.create({ data: { menuId, permissionId } });
});
}
generatorMenu(data, parentId = 0) {
const menu = [];
for (const item of data) {
if (item.parentId === parentId) {
const children = this.generatorMenu(data, item.id);
if (children.length) {
item.children = children;
}
menu.push(item);
}
}
return menu;
}
async getTreeMenu() {
const query: Prisma.SysMenuFindManyArgs = {
orderBy: [{ sort: 'desc' }, { id: 'asc' }],
};
const list = await this.prisma.sysMenu.findMany(query);
return this.generatorMenu(list);
}
findAll() {
const query: Prisma.SysMenuFindManyArgs = {
select: {
id: true,
name: true,
url: true,
parentId: true,
},
};
return this.prisma.sysMenu.findMany(query);
}
async update(id: number, data: UpdateMenuDto) {
const doesExists = await this.findOne({ id });
if (!doesExists) {
throw new HttpException('菜单不存在!', HttpStatus.NOT_FOUND);
}
if (data.parentId === id) {
throw new HttpException(
'父级id不可与自身id相同',
HttpStatus.BAD_REQUEST,
);
}
await this.prisma.sysMenu.update({ data, where: { id } });
}
async remove(menuId: number) {
const doesExists = await this.findOneById(menuId);
if (!doesExists) {
throw new HttpException('菜单不存在!', HttpStatus.NOT_FOUND);
}
// 判断是否存在子菜单
const doesExistsChildren = await this.prisma.sysMenu.findFirst({
where: { parentId: menuId },
});
if (doesExistsChildren) {
throw new HttpException(
'删除失败:请确保当前菜单下不存在子菜单!',
HttpStatus.FORBIDDEN,
);
}
await this.prisma.$transaction(async (prisma) => {
// step1 通过关联表删除权限表数据 这一步必须在前
// 因为schema文件中设置了onDelete: Cascade 删除菜单会删除关联表数据 则无法找出对应权限
await prisma.sysPermission.deleteMany({
where: {
sysMenuPermission: {
some: { menuId },
},
},
});
//step2 删除菜单数据
await prisma.sysMenu.delete({ where: { id: menuId } });
});
}
}

View File

@@ -0,0 +1,6 @@
import { IsNumber } from 'class-validator';
export class GrantMenuDto {
@IsNumber({}, { each: true })
menuIds: number[];
}

View File

@@ -0,0 +1,19 @@
import { Controller, Body, Patch, Param, Get } from '@nestjs/common';
import { PermissionService } from './permission.service';
import { GrantMenuDto } from '@/modules/system/permission/dto/grant-menu.dto';
@Controller()
export class PermissionController {
constructor(private readonly permissionService: PermissionService) {}
@Get(':id/menu')
getMenu(@Param('id') id: number) {
return this.permissionService.getMenu(id);
}
@Patch(':id')
grantMenu(@Param('id') id: number, @Body() dto: GrantMenuDto) {
return this.permissionService.grantMenu(id, dto);
}
}

View File

@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { PermissionService } from './permission.service';
import { PermissionController } from './permission.controller';
@Module({
controllers: [PermissionController],
providers: [PermissionService],
})
export class PermissionModule {}

View File

@@ -0,0 +1,45 @@
import { Injectable } from '@nestjs/common';
import { GrantMenuDto } from '@/modules/system/permission/dto/grant-menu.dto';
import { PrismaService } from '@/prisma/prisma.service';
@Injectable()
export class PermissionService {
constructor(private readonly prisma: PrismaService) {}
async getMenu(id: number) {
const menuIds = await this.prisma.sysMenuPermission.findMany({
select: {
menuId: true,
},
where: {
permission: {
sysRolePermission: {
some: { roleId: id },
},
},
},
});
return menuIds.map((item) => item.menuId);
}
async grantMenu(id: number, { menuIds }: GrantMenuDto) {
await this.prisma.$transaction(async (prisma) => {
// 删除角色对应的权限
await prisma.sysRolePermission.deleteMany({
where: { roleId: id },
});
// 通过菜单id 找出所有权限id
const rolePermIds = await prisma.sysMenuPermission.findMany({
select: { permissionId: true },
where: { menuId: { in: menuIds } },
});
// 根据权限id 关联菜单和权限
if (rolePermIds.length > 0) {
await prisma.sysRolePermission.createMany({
data: rolePermIds.map((item) => ({ ...item, roleId: id })),
});
}
});
}
}

View File

@@ -0,0 +1,12 @@
import { IsNotEmpty, IsNumber, IsOptional } from 'class-validator';
export class CreateRoleDto {
@IsOptional()
name: string;
@IsNotEmpty()
code: string;
@IsNumber()
status: number;
}

View File

@@ -0,0 +1,16 @@
import { PaginationDto } from '@/common/dto/pagination.dto';
import { IsInt, IsOptional } from 'class-validator';
import { Type } from 'class-transformer';
export class QueryListDto extends PaginationDto {
@IsOptional()
code?: string;
@IsOptional()
name?: string;
@IsOptional()
@Type(() => Number)
@IsInt()
status?: number;
}

View File

@@ -0,0 +1,6 @@
import { OmitType, PartialType } from '@nestjs/mapped-types';
import { CreateRoleDto } from './create-role.dto';
export class UpdateRoleDto extends PartialType(
OmitType(CreateRoleDto, ['code']),
) {}

View File

@@ -0,0 +1,44 @@
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
Query,
} from '@nestjs/common';
import { RoleService } from './role.service';
import { CreateRoleDto } from './dto/create-role.dto';
import { UpdateRoleDto } from './dto/update-role.dto';
import { QueryListDto } from './dto/query-list.dto';
@Controller()
export class RoleController {
constructor(private readonly roleService: RoleService) {}
@Post()
create(@Body() dto: CreateRoleDto) {
return this.roleService.create(dto);
}
@Get()
findPage(@Query() dto: QueryListDto): Promise<QueryListResult> {
return this.roleService.findPage(dto);
}
@Get('/all')
findAll() {
return this.roleService.findAll();
}
@Patch(':id')
update(@Param('id') id: string, @Body() dto: UpdateRoleDto) {
return this.roleService.update(+id, dto);
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.roleService.remove(+id);
}
}

View File

@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { RoleService } from './role.service';
import { RoleController } from './role.controller';
@Module({
controllers: [RoleController],
providers: [RoleService],
})
export class RoleModule {}

View File

@@ -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 { PrismaService } from '@/prisma/prisma.service';
import { Prisma, SysRole } from '@prisma/client';
import { QueryListDto } from './dto/query-list.dto';
@Injectable()
export class RoleService {
constructor(private readonly prisma: PrismaService) {}
async findOne(
params: Prisma.SysRoleWhereUniqueInput,
): Promise<SysRole | null> {
return this.prisma.sysRole.findUnique({
where: params,
});
}
async create(data: CreateRoleDto) {
const doesExists = await this.findOne({ code: data.code });
if (doesExists)
throw new HttpException('角色编码不可重复!', HttpStatus.NOT_FOUND);
await this.prisma.sysRole.create({ data });
}
async findPage(dto: QueryListDto): Promise<QueryListResult> {
const { page, pageSize, code, status, name } = dto;
const query: Prisma.SysRoleFindManyArgs = {
skip: pageSize * (page - 1),
take: pageSize,
where: {
...(code && { code: { contains: code } }),
...(!isNaN(status) && { status }),
...(name && { name: { contains: name } }),
},
};
const [list, count] = await this.prisma.$transaction([
this.prisma.sysRole.findMany(query),
this.prisma.sysRole.count({ where: query.where }),
]);
return {
page,
pageSize,
count,
list,
};
}
findAll() {
return this.prisma.sysRole.findMany({
select: {
id: true,
name: true,
code: true,
status: true,
},
});
}
async update(id: number, data: UpdateRoleDto) {
const role = await this.findOne({ id });
if (!role)
throw new HttpException('角色信息不存在!', HttpStatus.NOT_FOUND);
await this.prisma.sysRole.update({
data,
where: { id },
});
}
async remove(id: number) {
const role = await this.findOne({ id });
if (!role)
throw new HttpException('角色信息不存在!', HttpStatus.NOT_FOUND);
await this.prisma.sysRole.delete({ where: { id } });
}
}

View File

@@ -0,0 +1,58 @@
import { Module } from '@nestjs/common';
import { RouterModule } from '@nestjs/core';
import { UserModule } from './user/user.module';
import { AuthModule } from './auth/auth.module';
import { RoleModule } from './role/role.module';
import { FilesModule } from './files/files.module';
import { MenuModule } from './menu/menu.module';
import { ConfigModule } from './config/config.module';
import { PermissionModule } from './permission/permission.module';
@Module({
imports: [
RouterModule.register([
{
path: 'admin',
module: SystemModule,
children: [
{
path: 'user',
module: UserModule,
},
{
path: 'auth',
module: AuthModule,
},
{
path: 'role',
module: RoleModule,
},
{
path: 'files',
module: FilesModule,
},
{
path: 'menu',
module: MenuModule,
},
{
path: 'config',
module: ConfigModule,
},
{
path: 'permission',
module: PermissionModule,
},
],
},
]),
UserModule,
AuthModule,
RoleModule,
FilesModule,
MenuModule,
ConfigModule,
PermissionModule,
],
})
export class SystemModule {}

View File

@@ -0,0 +1,35 @@
import {
IsNotEmpty,
IsNumber,
IsOptional,
MaxLength,
MinLength,
} from 'class-validator';
export class CreateUserDto {
@IsOptional()
@MaxLength(20, { message: '用户名不要超过20个字符' })
name?: string;
@IsNotEmpty()
@MinLength(5, { message: '账户名不要低于6位字符' })
@MaxLength(50, { message: '账户名不要超过50个字符' })
account: string;
@IsOptional()
@IsNumber()
avatarId: number;
@IsNotEmpty()
@IsNumber()
gender: number;
@IsNotEmpty()
@MinLength(6, { message: '密码不要低于6位字符' })
@MaxLength(20, { message: '密码不要超过50个字符' })
password: string;
@IsOptional()
@IsNumber()
status?: number;
}

View File

@@ -0,0 +1,16 @@
import { PaginationDto } from '@/common/dto/pagination.dto';
import { IsOptional, IsInt } from 'class-validator';
import { Type } from 'class-transformer';
export class QueryListDto extends PaginationDto {
@IsOptional()
account?: string;
@IsOptional()
name?: string;
@IsOptional()
@Type(() => Number)
@IsInt()
status?: number;
}

View File

@@ -0,0 +1,10 @@
import { IsOptional, MaxLength, MinLength } from 'class-validator';
import { PickType } from '@nestjs/mapped-types';
import { CreateUserDto } from './create-user.dto';
export class UpdatePasswordDto extends PickType(CreateUserDto, ['password']) {
@IsOptional()
@MinLength(5)
@MaxLength(50)
newPassword?: string;
}

View File

@@ -0,0 +1,6 @@
import { OmitType, PartialType } from '@nestjs/mapped-types';
import { CreateUserDto } from './create-user.dto';
export class UpdateUserDto extends PartialType(
OmitType(CreateUserDto, ['account', 'password'] as const),
) {}

View File

@@ -0,0 +1,58 @@
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
Query,
Request,
} from '@nestjs/common';
import { UserService } from './user.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { QueryListDto } from './dto/query-list.dto';
import { UpdatePasswordDto } from './dto/update-password.dto';
@Controller()
export class UserController {
constructor(private readonly userService: UserService) {}
@Post()
create(@Body() data: CreateUserDto) {
return this.userService.create(data);
}
@Get()
findAll(@Query() dto: QueryListDto): Promise<QueryListResult> {
return this.userService.findAll(dto);
}
@Get('/info')
getUserInfo(@Request() req) {
const { sub } = req['user'];
const { isAdmin } = req;
return this.userService.getUserInfo(sub, isAdmin);
}
@Get(':id')
findOneById(@Param('id') id: number) {
return this.userService.findOneById(id);
}
@Patch(':id/password')
updatePassword(@Param('id') id: string, @Body() dto: UpdatePasswordDto) {
return this.userService.updatePassword(+id, dto);
}
@Patch(':id')
update(@Param('id') id: string, @Body() dto: UpdateUserDto) {
return this.userService.update(+id, dto);
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.userService.remove(+id);
}
}

View File

@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { UserService } from './user.service';
import { UserController } from './user.controller';
@Module({
controllers: [UserController],
providers: [UserService],
})
export class UserModule {}

View File

@@ -0,0 +1,214 @@
import {
HttpException,
HttpStatus,
Inject,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { Prisma, SysUser } from '@prisma/client';
import { PrismaService } from '@/prisma/prisma.service';
import { QueryListDto } from './dto/query-list.dto';
import * as bcrypt from 'bcryptjs';
import { UpdatePasswordDto } from './dto/update-password.dto';
import { SkipAuth } from '@/common/decorators/skip-auth.decorator';
import { RedisGroup } from '@/common/enum';
import { Cache, CACHE_MANAGER } from '@nestjs/cache-manager';
@Injectable()
@SkipAuth()
export class UserService {
constructor(
private readonly prisma: PrismaService,
@Inject(CACHE_MANAGER) private cacheManager: Cache,
) {}
async findOne(
params: Prisma.SysUserWhereUniqueInput,
): Promise<SysUser | null> {
return this.prisma.sysUser.findUnique({
where: params,
});
}
async create(dto: CreateUserDto) {
const userExists = await this.findOne({ account: dto.account });
if (userExists)
throw new HttpException('账号已存在!', HttpStatus.UNPROCESSABLE_ENTITY);
const passwordSalt = await bcrypt.genSalt();
const password = await bcrypt.hash(dto.password, passwordSalt);
const data = {
...dto,
passwordSalt,
password,
};
await this.prisma.sysUser.create({ data });
}
async findAll(dto: QueryListDto): Promise<QueryListResult> {
const { page, pageSize, account, status, name } = dto;
const query: Prisma.SysUserFindManyArgs = {
select: {
id: true,
name: true,
account: true,
status: true,
gender: true,
createdAt: true,
updatedAt: true,
avatar: {
select: {
id: true,
path: true,
},
},
userRole: {
select: { roleId: true },
},
},
skip: pageSize * (page - 1),
take: pageSize,
where: {
...(account && { account: { contains: account } }),
...(!isNaN(status) && { status }),
...(name && { name: { contains: name } }),
},
};
const [list, count] = await this.prisma.$transaction([
this.prisma.sysUser.findMany(query),
this.prisma.sysUser.count({ where: query.where }),
]);
return {
page,
pageSize,
count,
list,
};
}
async findOneById(id: number) {
return this.prisma.sysUser.findUnique({
select: {
id: true,
name: true,
account: true,
status: true,
gender: true,
avatar: {
select: {
id: true,
path: true,
},
},
},
where: {
id,
},
});
}
async getUserInfo(id: number, isAdmin: boolean) {
const user = await this.prisma.sysUser.findUnique({
select: {
name: true,
gender: true,
account: true,
avatar: {
select: {
path: true,
},
},
createdAt: true,
},
where: {
id,
status: 1,
},
});
const query: Prisma.SysMenuFindManyArgs = { where: {} };
if (!isAdmin) {
query.where = {
sysMenuPermission: {
some: {
permission: {
sysRolePermission: {
some: { role: { sysUserRole: { some: { userId: id } } } },
},
},
},
},
};
}
const menus = await this.prisma.sysMenu.findMany(query);
return {
...user,
menus,
};
}
async update(id: number, data: UpdateUserDto) {
const user = await this.findOne({ id });
if (!user)
throw new HttpException('修改失败,用户不存在!', HttpStatus.NOT_FOUND);
await this.prisma.$transaction(async (prisma) => {
await prisma.sysUser.update({
data,
where: { id },
});
// 禁用用户 删除token
if (data.status === 0) {
if (data.status === 0) {
const keys = await this.cacheManager.store.keys(
`${RedisGroup.JwtJti}:${id}:*`,
);
await this.cacheManager.store.mdel(...keys);
}
}
});
}
async updatePassword(
id: number,
{ password, newPassword }: UpdatePasswordDto,
) {
const user = await this.findOne({ id });
if (!user)
throw new HttpException('修改失败,用户不存在!', HttpStatus.NOT_FOUND);
// 同时存在密码和新密码
const data: Partial<SysUser> = {};
const comparePasswordSalt = user.passwordSalt;
const comparePassword = await bcrypt.hash(password, comparePasswordSalt);
if (comparePassword !== user.password) {
throw new UnauthorizedException('密码错误,修改失败!');
} else {
data.passwordSalt = await bcrypt.genSalt();
data.password = await bcrypt.hash(newPassword, data.passwordSalt);
}
await this.prisma.sysUser.update({
data,
where: { id },
});
}
async remove(id: number) {
const user = await this.findOne({ id });
if (!user)
throw new HttpException('删除失败,用户不存在!', HttpStatus.NOT_FOUND);
await this.prisma.sysUser.delete({ where: { id } });
}
}