format + lint

This commit is contained in:
2025-06-14 14:12:18 +08:00
parent e777afc433
commit 90a67b681e
69 changed files with 1756 additions and 1583 deletions

View File

@@ -16,9 +16,7 @@ import { BlogModule } from 'src/blog/blog.module';
@Module({ @Module({
imports: [ imports: [
TypeOrmModule.forFeature([ TypeOrmModule.forFeature([User]),
User,
]),
UserModule, UserModule,
RoleModule, RoleModule,
ResourceModule, ResourceModule,
@@ -35,4 +33,4 @@ import { BlogModule } from 'src/blog/blog.module';
AdminWebBlogController, AdminWebBlogController,
], ],
}) })
export class AdminModule { } export class AdminModule {}

View File

@@ -1,31 +1,31 @@
import { Body, Controller, Delete, Get, Param, ParseUUIDPipe, Post } from "@nestjs/common"; import {
import { PermissionService } from "src/role/services/permission.service"; Body,
import { CreatePermissionDto } from "../dto/admin-permission/create-permission.dto"; Controller,
Delete,
Get,
Param,
ParseUUIDPipe,
Post,
} from '@nestjs/common';
import { PermissionService } from 'src/role/services/permission.service';
import { CreatePermissionDto } from '../dto/admin-permission/create-permission.dto';
@Controller('admin/permission') @Controller('admin/permission')
export class AdminPermissionController { export class AdminPermissionController {
constructor(private readonly permissionService: PermissionService) {}
constructor( @Get()
private readonly permissionService: PermissionService, async list() {
) { } return this.permissionService.list();
}
@Get() @Post()
async list() { async create(@Body() dto: CreatePermissionDto) {
return this.permissionService.list(); return this.permissionService.create(dto);
} }
@Post() @Delete(':id')
async create( async delete(@Param('id', new ParseUUIDPipe({ version: '4' })) id: string) {
@Body() dto: CreatePermissionDto return this.permissionService.delete(id);
) { }
return this.permissionService.create(dto); }
}
@Delete(':id')
async delete(
@Param('id', new ParseUUIDPipe({ version: '4' })) id: string,
) {
return this.permissionService.delete(id);
}
}

View File

@@ -1,37 +1,51 @@
import { Body, Controller, Delete, Get, Param, ParseUUIDPipe, Post } from "@nestjs/common"; import {
import { PermissionService } from "src/role/services/permission.service"; Body,
import { RolePermissionService } from "src/role/services/role-permission.service"; Controller,
import { SetRolePermissionsDto } from "../dto/admin-role-permission/set-role-permissions.dto"; Delete,
Get,
Param,
ParseUUIDPipe,
Post,
} from '@nestjs/common';
import { PermissionService } from 'src/role/services/permission.service';
import { RolePermissionService } from 'src/role/services/role-permission.service';
import { SetRolePermissionsDto } from '../dto/admin-role-permission/set-role-permissions.dto';
@Controller('admin/roles/:roleId/permission') @Controller('admin/roles/:roleId/permission')
export class AdminRolePermissionController { export class AdminRolePermissionController {
constructor(
private readonly rolePermissionService: RolePermissionService,
private readonly permissionService: PermissionService,
) {}
constructor( @Get()
private readonly rolePermissionService: RolePermissionService, async getRolePermissions(
private readonly permissionService: PermissionService, @Param('roleId', new ParseUUIDPipe({ version: '4' })) roleId: string,
) { } ) {
const permissionIds =
await this.rolePermissionService.findPermissionIdsByRoleIds([roleId]);
return await this.permissionService.findPermissionByIds(permissionIds);
}
@Get() @Post()
async getRolePermissions( async setRolePermissions(
@Param('roleId', new ParseUUIDPipe({ version: '4' })) roleId: string, @Param('roleId', new ParseUUIDPipe({ version: '4' })) roleId: string,
) { @Body() dto: SetRolePermissionsDto,
const permissionIds = await this.rolePermissionService.findPermissionIdsByRoleIds([roleId]); ) {
return await this.permissionService.findPermissionByIds(permissionIds); return await this.rolePermissionService.addRolePermissions(
} roleId,
dto.permissionIds,
);
}
@Post() @Delete()
async setRolePermissions( async DeleteRolePermissionsDto(
@Param('roleId', new ParseUUIDPipe({ version: '4' })) roleId: string, @Param('roleId', new ParseUUIDPipe({ version: '4' })) roleId: string,
@Body() dto: SetRolePermissionsDto, @Body() dto: SetRolePermissionsDto,
) { ) {
return await this.rolePermissionService.addRolePermissions(roleId, dto.permissionIds); return await this.rolePermissionService.deleteRolePermissions(
} roleId,
dto.permissionIds,
@Delete() );
async DeleteRolePermissionsDto( }
@Param('roleId', new ParseUUIDPipe({ version: '4' })) roleId: string, }
@Body() dto: SetRolePermissionsDto,
) {
return await this.rolePermissionService.deleteRolePermissions(roleId, dto.permissionIds);
}
}

View File

@@ -1,30 +1,31 @@
import { Body, Controller, Delete, Get, Param, ParseUUIDPipe, Post } from "@nestjs/common"; import {
import { RoleService } from "src/role/services/role.service"; Body,
import { CreateRoleDto } from "../dto/admin-role/create-role.dto"; Controller,
Delete,
Get,
Param,
ParseUUIDPipe,
Post,
} from '@nestjs/common';
import { RoleService } from 'src/role/services/role.service';
import { CreateRoleDto } from '../dto/admin-role/create-role.dto';
@Controller('admin/role') @Controller('admin/role')
export class AdminRoleController { export class AdminRoleController {
constructor(private readonly roleService: RoleService) {}
constructor( @Get()
private readonly roleService: RoleService, async list() {
) { } return this.roleService.list();
}
@Get() @Post()
async list() { async create(@Body() dto: CreateRoleDto) {
return this.roleService.list(); return this.roleService.create(dto);
} }
@Post() @Delete(':id')
async create( async delete(@Param('id', new ParseUUIDPipe({ version: '4' })) id: string) {
@Body() dto: CreateRoleDto return this.roleService.delete(id);
) { }
return this.roleService.create(dto); }
}
@Delete(':id')
async delete(
@Param('id', new ParseUUIDPipe({ version: '4' })) id: string,
) {
return this.roleService.delete(id);
}
}

View File

@@ -1,43 +1,50 @@
import { Body, Controller, Delete, Get, Param, ParseUUIDPipe, Post } from "@nestjs/common"; import {
import { RoleService } from "src/role/services/role.service"; Body,
import { UserRoleService } from "src/role/services/user-role.service"; Controller,
import { CreateUserRoleDto } from "../dto/admin-user-role/create-user-role.dto"; Delete,
import { DeleteUserRoleDto } from "../dto/admin-user-role/delete-user-role.dto"; Get,
Param,
ParseUUIDPipe,
Post,
} from '@nestjs/common';
import { RoleService } from 'src/role/services/role.service';
import { UserRoleService } from 'src/role/services/user-role.service';
import { CreateUserRoleDto } from '../dto/admin-user-role/create-user-role.dto';
import { DeleteUserRoleDto } from '../dto/admin-user-role/delete-user-role.dto';
@Controller('admin/users/:userId/role') @Controller('admin/users/:userId/role')
export class AdminUserRoleController { export class AdminUserRoleController {
constructor(
private readonly userRoleService: UserRoleService,
private readonly roleService: RoleService,
) {}
constructor( @Get()
private readonly userRoleService: UserRoleService, async getUserRoles(
private readonly roleService: RoleService, @Param('userId', new ParseUUIDPipe({ version: '4' })) userId: string,
) { } ) {
const userRoleIds = await this.userRoleService.findRoleIdsByUserId(userId);
return await this.roleService.findRolesByRoleIds(userRoleIds);
}
@Get() @Post()
async getUserRoles( async setUserRoles(
@Param('userId', new ParseUUIDPipe({ version: '4' })) userId: string, @Param('userId', new ParseUUIDPipe({ version: '4' })) userId: string,
) { @Body() dto: CreateUserRoleDto,
const userRoleIds = await this.userRoleService.findRoleIdsByUserId(userId); ) {
return await this.roleService.findRolesByRoleIds(userRoleIds); return this.userRoleService.addUserRole({
} userId,
roleId: dto.roleId,
isEnabled: dto.isEnabled,
expiredAt: dto.expiredAt,
});
}
@Post() @Delete()
async setUserRoles( async deleteUserRoles(
@Param('userId', new ParseUUIDPipe({ version: '4' })) userId: string, @Param('userId', new ParseUUIDPipe({ version: '4' })) userId: string,
@Body() dto: CreateUserRoleDto, @Body() dto: DeleteUserRoleDto,
) { ) {
return this.userRoleService.addUserRole({ return this.userRoleService.deleteUserRole(userId, dto.roleId);
userId, }
roleId: dto.roleId, }
isEnabled: dto.isEnabled,
expiredAt: dto.expiredAt,
});
}
@Delete()
async deleteUserRoles(
@Param('userId', new ParseUUIDPipe({ version: '4' })) userId: string,
@Body() dto: DeleteUserRoleDto,
) {
return this.userRoleService.deleteUserRole(userId, dto.roleId);
}
}

View File

@@ -1,69 +1,76 @@
import { Body, Controller, Delete, Get, Param, ParseUUIDPipe, Post, Put, Query } from "@nestjs/common"; import {
import { ListDto } from "../dto/admin-user/list.dto"; Body,
import { CreateDto } from "../dto/admin-user/create.dto"; Controller,
import { UserService } from "src/user/user.service"; Delete,
import { UpdateDto } from "../dto/admin-user/update.dto"; Get,
import { UpdatePasswordDto } from "../dto/admin-user/update-password.dto"; Param,
import { RemoveUserDto } from "../dto/admin-user/remove.dto"; ParseUUIDPipe,
Post,
Put,
Query,
} from '@nestjs/common';
import { ListDto } from '../dto/admin-user/list.dto';
import { CreateDto } from '../dto/admin-user/create.dto';
import { UserService } from 'src/user/user.service';
import { UpdateDto } from '../dto/admin-user/update.dto';
import { UpdatePasswordDto } from '../dto/admin-user/update-password.dto';
import { RemoveUserDto } from '../dto/admin-user/remove.dto';
@Controller('admin/user') @Controller('admin/user')
export class AdminUserController { export class AdminUserController {
constructor(private readonly userService: UserService) {}
constructor( @Get()
private readonly userService: UserService, async list(@Query() listDto: ListDto) {
) { } return this.userService.list(listDto.page, listDto.pageSize);
}
@Get() @Get(':userId')
async list( async get(
@Query() listDto: ListDto @Param('userId', new ParseUUIDPipe({ version: '4' })) userId: string,
) { ) {
return this.userService.list(listDto.page, listDto.pageSize); return this.userService.findOne({ userId });
} }
@Get(':userId') @Post()
async get( async create(@Body() createDto: CreateDto) {
@Param('userId', new ParseUUIDPipe({ version: '4' })) userId: string, return this.userService.create({
) { ...createDto,
return this.userService.findOne({ userId }); ...(createDto.password &&
} (() => {
const salt = this.userService.generateSalt();
return {
salt,
password_hash: this.userService.hashPassword(
createDto.password,
salt,
),
};
})()),
});
}
@Post() @Put(':userId')
async create( async update(
@Body() createDto: CreateDto @Param('userId', new ParseUUIDPipe({ version: '4' })) userId: string,
) { @Body() updateDto: UpdateDto,
return this.userService.create({ ) {
...createDto, return this.userService.update(userId, updateDto);
...createDto.password && (() => { }
const salt = this.userService.generateSalt();
return {
salt,
password_hash: this.userService.hashPassword(createDto.password, salt),
}
})(),
});
}
@Put(':userId') @Delete(':userId')
async update( async delete(
@Param('userId', new ParseUUIDPipe({ version: '4' })) userId: string, @Param('userId', new ParseUUIDPipe({ version: '4' })) userId: string,
@Body() updateDto: UpdateDto, @Query() dto: RemoveUserDto,
) { ) {
return this.userService.update(userId, updateDto); return this.userService.delete(userId, dto.soft);
} }
@Delete(':userId') @Post(':userId/password')
async delete( async setPassword(
@Param('userId', new ParseUUIDPipe({ version: '4' })) userId: string, @Param('userId', new ParseUUIDPipe({ version: '4' })) userId: string,
@Query() dto: RemoveUserDto, @Body() updatePasswordDto: UpdatePasswordDto,
) { ) {
return this.userService.delete(userId, dto.soft); return this.userService.setPassword(userId, updatePasswordDto.password);
} }
}
@Post(':userId/password')
async setPassword(
@Param('userId', new ParseUUIDPipe({ version: '4' })) userId: string,
@Body() updatePasswordDto: UpdatePasswordDto,
) {
return this.userService.setPassword(userId, updatePasswordDto.password);
}
}

View File

@@ -1,45 +1,45 @@
import { Body, Controller, Delete, Get, Param, ParseUUIDPipe, Post, Put } from "@nestjs/common"; import {
import { CreateBlogDto } from "src/admin/dto/admin-web/create-blog.dto"; Body,
import { BlogService } from "src/blog/blog.service"; Controller,
Delete,
Get,
Param,
ParseUUIDPipe,
Post,
Put,
} from '@nestjs/common';
import { CreateBlogDto } from 'src/admin/dto/admin-web/create-blog.dto';
import { BlogService } from 'src/blog/blog.service';
@Controller('/admin/web/blog') @Controller('/admin/web/blog')
export class AdminWebBlogController { export class AdminWebBlogController {
constructor(private readonly adminWebBlogService: BlogService) {}
constructor( @Get()
private readonly adminWebBlogService: BlogService, async list() {
) { } return this.adminWebBlogService.list();
}
@Get() @Post()
async list() { async create(@Body() dto: CreateBlogDto) {
return this.adminWebBlogService.list(); return this.adminWebBlogService.create(dto);
} }
@Post() @Put(':id')
async create( async update(
@Body() dto: CreateBlogDto, @Param('id', new ParseUUIDPipe({ version: '4' })) id: string,
) { @Body() dto: CreateBlogDto,
return this.adminWebBlogService.create(dto); ) {
} return this.adminWebBlogService.update(id, dto);
}
@Put(':id') @Get(':id')
async update( async get(@Param('id', new ParseUUIDPipe({ version: '4' })) id: string) {
@Param('id', new ParseUUIDPipe({ version: '4' })) id: string, return this.adminWebBlogService.findById(id);
@Body() dto: CreateBlogDto, }
) {
return this.adminWebBlogService.update(id, dto);
}
@Get(':id') @Delete(':id')
async get( async remove(@Param('id', new ParseUUIDPipe({ version: '4' })) id: string) {
@Param('id', new ParseUUIDPipe({ version: '4' })) id: string, return this.adminWebBlogService.remove(id);
) { }
return this.adminWebBlogService.findById(id); }
}
@Delete(':id')
async remove(
@Param('id', new ParseUUIDPipe({ version: '4' })) id: string,
) {
return this.adminWebBlogService.remove(id);
}
}

View File

@@ -1,41 +1,45 @@
import { Body, Controller, Delete, Get, Param, ParseUUIDPipe, Post, Put } from "@nestjs/common"; import {
import { CreateResourceDto } from "src/admin/dto/admin-web/create-resource.dto"; Body,
import { ResourceService } from "src/resource/resource.service"; Controller,
Delete,
Get,
Param,
ParseUUIDPipe,
Post,
Put,
} from '@nestjs/common';
import { CreateResourceDto } from 'src/admin/dto/admin-web/create-resource.dto';
import { ResourceService } from 'src/resource/resource.service';
@Controller('/admin/web/resource') @Controller('/admin/web/resource')
export class AdminWebResourceController { export class AdminWebResourceController {
constructor(private readonly resourceService: ResourceService) {}
constructor( @Get()
private readonly resourceService: ResourceService, async list() {
) { } return this.resourceService.findAll();
}
@Get() @Get(':id')
async list() { async get(@Param('id', new ParseUUIDPipe({ version: '4' })) id: string) {
return this.resourceService.findAll(); return this.resourceService.findById(id);
} }
@Get(':id') @Post()
async get(@Param('id', new ParseUUIDPipe({ version: '4' })) id: string) { async create(@Body() data: CreateResourceDto) {
return this.resourceService.findById(id); return this.resourceService.create(data);
} }
@Post() @Put(':id')
async create(@Body() data: CreateResourceDto) { async update(
return this.resourceService.create(data); @Param('id', new ParseUUIDPipe({ version: '4' })) id: string,
} @Body() data: CreateResourceDto,
) {
return this.resourceService.update(id, data);
}
@Put(':id') @Delete(':id')
async update( async delete(@Param('id', new ParseUUIDPipe({ version: '4' })) id: string) {
@Param('id', new ParseUUIDPipe({ version: '4' })) id: string, return this.resourceService.delete(id);
@Body() data: CreateResourceDto }
) { }
return this.resourceService.update(id, data);
}
@Delete(':id')
async delete(
@Param('id', new ParseUUIDPipe({ version: '4' })) id: string,
) {
return this.resourceService.delete(id);
}
}

View File

@@ -1,9 +1,9 @@
import { IsString } from "class-validator"; import { IsString } from 'class-validator';
export class CreatePermissionDto { export class CreatePermissionDto {
@IsString() @IsString()
name: string name: string;
@IsString() @IsString()
description: string; description: string;
} }

View File

@@ -1,8 +1,8 @@
import { ArrayMinSize, IsArray, IsUUID } from "class-validator"; import { ArrayMinSize, IsArray, IsUUID } from 'class-validator';
export class DeleteRolePermissionsDto { export class DeleteRolePermissionsDto {
@IsArray() @IsArray()
@ArrayMinSize(1) @ArrayMinSize(1)
@IsUUID('4', { each: true }) @IsUUID('4', { each: true })
permissionIds: string[]; permissionIds: string[];
} }

View File

@@ -1,8 +1,8 @@
import { ArrayMinSize, IsArray, IsUUID } from "class-validator"; import { ArrayMinSize, IsArray, IsUUID } from 'class-validator';
export class SetRolePermissionsDto { export class SetRolePermissionsDto {
@IsArray() @IsArray()
@ArrayMinSize(1) @ArrayMinSize(1)
@IsUUID('4', { each: true }) @IsUUID('4', { each: true })
permissionIds: string[]; permissionIds: string[];
} }

View File

@@ -1,9 +1,9 @@
import { IsString } from "class-validator"; import { IsString } from 'class-validator';
export class CreateRoleDto { export class CreateRoleDto {
@IsString() @IsString()
name: string name: string;
@IsString() @IsString()
localName: string; localName: string;
} }

View File

@@ -1,13 +1,13 @@
import { IsBoolean, IsDateString, IsOptional, IsUUID } from "class-validator"; import { IsBoolean, IsDateString, IsOptional, IsUUID } from 'class-validator';
export class CreateUserRoleDto { export class CreateUserRoleDto {
@IsUUID('4') @IsUUID('4')
roleId: string; roleId: string;
@IsBoolean() @IsBoolean()
isEnabled: boolean; isEnabled: boolean;
@IsOptional() @IsOptional()
@IsDateString() @IsDateString()
expiredAt?: Date; expiredAt?: Date;
} }

View File

@@ -1,6 +1,6 @@
import { IsUUID } from "class-validator"; import { IsUUID } from 'class-validator';
export class DeleteUserRoleDto { export class DeleteUserRoleDto {
@IsUUID('4') @IsUUID('4')
roleId: string; roleId: string;
} }

View File

@@ -1,31 +1,32 @@
import { IsString, Length, Matches, ValidateIf } from "class-validator"; import { IsString, Length, Matches, ValidateIf } from 'class-validator';
export class CreateDto { export class CreateDto {
@ValidateIf(o => o.username !== null) @ValidateIf((o) => o.username !== null)
@IsString({ message: '用户名不得为空' }) @IsString({ message: '用户名不得为空' })
@Length(4, 32, { message: '用户名长度只能为4~32' }) @Length(4, 32, { message: '用户名长度只能为4~32' })
username: string | null; username: string | null;
@ValidateIf(o => o.nickname !== null) @ValidateIf((o) => o.nickname !== null)
@IsString({ message: '昵称不得为空' }) @IsString({ message: '昵称不得为空' })
@Length(1, 30, { message: '昵称长度只能为1~30' }) @Length(1, 30, { message: '昵称长度只能为1~30' })
nickname: string | null; nickname: string | null;
@ValidateIf(o => o.email !== null) @ValidateIf((o) => o.email !== null)
@IsString({ message: '邮箱不得为空' }) @IsString({ message: '邮箱不得为空' })
@Length(6, 254, { message: '邮箱长度只能为6~254' }) @Length(6, 254, { message: '邮箱长度只能为6~254' })
email: string | null; email: string | null;
@ValidateIf(o => o.phone !== null) @ValidateIf((o) => o.phone !== null)
@IsString({ message: '手机号不得为空' }) @IsString({ message: '手机号不得为空' })
@Length(11, 11, { message: '手机号长度只能为11' }) @Length(11, 11, { message: '手机号长度只能为11' })
phone: string | null; phone: string | null;
@ValidateIf(o => o.password !== null) @ValidateIf((o) => o.password !== null)
@IsString({ message: '密码不得为空' }) @IsString({ message: '密码不得为空' })
@Length(6, 32, { message: '密码长度只能为6~32' }) @Length(6, 32, { message: '密码长度只能为6~32' })
@Matches(/^(?=.*[a-zA-Z])(?=.*\d)[a-zA-Z\d!@#$%^&*()_+\-=\[\]{};:'",.<>/?]{6,32}$/, @Matches(
{ message: '密码必须包含字母和数字且长度在6~32之间' } /^(?=.*[a-zA-Z])(?=.*\d)[a-zA-Z\d!@#$%^&*()_+\-=\[\]{};:'",.<>/?]{6,32}$/,
) { message: '密码必须包含字母和数字且长度在6~32之间' },
password: string | null; )
} password: string | null;
}

View File

@@ -1,5 +1,3 @@
import { PaginationDto } from "../common/pagination.dto"; import { PaginationDto } from '../common/pagination.dto';
export class ListDto extends PaginationDto { export class ListDto extends PaginationDto {}
}

View File

@@ -1,8 +1,8 @@
import { Transform } from "class-transformer"; import { Transform } from 'class-transformer';
import { IsBoolean } from "class-validator"; import { IsBoolean } from 'class-validator';
export class RemoveUserDto { export class RemoveUserDto {
@Transform(({ value }) => value === 'true') @Transform(({ value }) => value === 'true')
@IsBoolean({ message: '需指定删除类型' }) @IsBoolean({ message: '需指定删除类型' })
soft: boolean; soft: boolean;
} }

View File

@@ -1,10 +1,11 @@
import { IsString, Length, Matches } from "class-validator"; import { IsString, Length, Matches } from 'class-validator';
export class UpdatePasswordDto { export class UpdatePasswordDto {
@IsString({ message: '密码不得为空' }) @IsString({ message: '密码不得为空' })
@Length(6, 32, { message: '密码长度只能为6~32' }) @Length(6, 32, { message: '密码长度只能为6~32' })
@Matches(/^(?=.*[a-zA-Z])(?=.*\d)[a-zA-Z\d!@#$%^&*()_+\-=\[\]{};:'",.<>/?]{6,32}$/, @Matches(
{ message: '密码必须包含字母和数字且长度在6~32之间' } /^(?=.*[a-zA-Z])(?=.*\d)[a-zA-Z\d!@#$%^&*()_+\-=\[\]{};:'",.<>/?]{6,32}$/,
) { message: '密码必须包含字母和数字且长度在6~32之间' },
password: string; )
} password: string;
}

View File

@@ -1,29 +1,35 @@
import { IsEmail, IsOptional, IsString, Length, Matches } from "class-validator"; import {
IsEmail,
IsOptional,
IsString,
Length,
Matches,
} from 'class-validator';
export class UpdateDto { export class UpdateDto {
@IsString({ message: '用户名不得为空' }) @IsString({ message: '用户名不得为空' })
@Length(4, 32, { message: '用户名长度只能为4~32' }) @Length(4, 32, { message: '用户名长度只能为4~32' })
username: string; username: string;
@IsString({ message: '昵称不得为空' }) @IsString({ message: '昵称不得为空' })
@Length(1, 30, { message: '昵称长度只能为1~30' }) @Length(1, 30, { message: '昵称长度只能为1~30' })
nickname: string; nickname: string;
@IsOptional() @IsOptional()
@IsEmail({}, { message: '请输入有效的邮箱地址', always: false }) @IsEmail({}, { message: '请输入有效的邮箱地址', always: false })
@Length(6, 254, { @Length(6, 254, {
message: '邮箱长度只能为6~254', message: '邮箱长度只能为6~254',
// 仅在值不为 null 或 undefined 时验证 // 仅在值不为 null 或 undefined 时验证
always: false always: false,
}) })
email?: string; email?: string;
@IsOptional() // 标记字段为可选 @IsOptional() // 标记字段为可选
@IsString({ message: '手机号不得为空', always: false }) @IsString({ message: '手机号不得为空', always: false })
@Matches(/^1[3456789]\d{9}$/, { @Matches(/^1[3456789]\d{9}$/, {
message: '请输入有效的手机号码', message: '请输入有效的手机号码',
// 仅在值不为 null 或 undefined 时验证 // 仅在值不为 null 或 undefined 时验证
always: false always: false,
}) })
phone?: string; phone?: string;
} }

View File

@@ -1,12 +1,12 @@
import { IsString } from "class-validator"; import { IsString } from 'class-validator';
export class CreateBlogDto { export class CreateBlogDto {
@IsString() @IsString()
title: string; title: string;
@IsString() @IsString()
description: string; description: string;
@IsString() @IsString()
contentUrl: string; contentUrl: string;
} }

View File

@@ -1,28 +1,28 @@
import { Type } from "class-transformer"; import { Type } from 'class-transformer';
import { IsString, ValidateNested } from "class-validator"; import { IsString, ValidateNested } from 'class-validator';
class ResourceTagDto { class ResourceTagDto {
@IsString() @IsString()
name: string; name: string;
@IsString() @IsString()
type: string; type: string;
} }
export class CreateResourceDto { export class CreateResourceDto {
@IsString() @IsString()
title: string; title: string;
@IsString() @IsString()
description: string; description: string;
@IsString() @IsString()
imageUrl: string; imageUrl: string;
@IsString() @IsString()
link: string; link: string;
@ValidateNested({ each: true }) @ValidateNested({ each: true })
@Type(() => ResourceTagDto) @Type(() => ResourceTagDto)
tags: ResourceTagDto[]; tags: ResourceTagDto[];
} }

View File

@@ -1,16 +1,16 @@
import { Type } from 'class-transformer'; import { Type } from 'class-transformer';
import { IsInt, IsOptional, Max, Min } from 'class-validator'; import { IsInt, IsOptional, Min } from 'class-validator';
export class PaginationDto { export class PaginationDto {
@IsOptional() @IsOptional()
@Type(() => Number) @Type(() => Number)
@IsInt() @IsInt()
@Min(1) @Min(1)
page?: number = 1; page?: number = 1;
@IsOptional() @IsOptional()
@Type(() => Number) @Type(() => Number)
@IsInt() @IsInt()
@Min(1) @Min(1)
pageSize?: number = 20; pageSize?: number = 20;
} }

View File

@@ -42,4 +42,4 @@ import { OssModule } from './oss/oss.module';
controllers: [AppController], controllers: [AppController],
providers: [AppService], providers: [AppService],
}) })
export class AppModule { } export class AppModule {}

View File

@@ -1,4 +1,11 @@
import { BadRequestException, Body, Controller, Get, Post, Request, UseGuards } from '@nestjs/common'; import {
BadRequestException,
Body,
Controller,
Post,
Request,
UseGuards,
} from '@nestjs/common';
import { LoginDto } from './dto/login.dto'; import { LoginDto } from './dto/login.dto';
import { AuthService } from './auth.service'; import { AuthService } from './auth.service';
import { AuthGuard } from '@nestjs/passport'; import { AuthGuard } from '@nestjs/passport';
@@ -6,32 +13,31 @@ import { UserSessionService } from 'src/user/services/user-session.service';
@Controller('auth') @Controller('auth')
export class AuthController { export class AuthController {
constructor(
private readonly authService: AuthService,
private readonly userSessionService: UserSessionService,
) {}
constructor( @Post('login')
private readonly authService: AuthService, async login(@Body() loginDto: LoginDto) {
private readonly userSessionService: UserSessionService, switch (loginDto.type) {
) { } case 'password':
return this.authService.loginWithPassword(loginDto);
@Post('login') case 'phone':
async login(@Body() loginDto: LoginDto) { return this.authService.loginWithPhone(loginDto);
switch (loginDto.type) { case 'email':
case 'password': return this.authService.loginWithEmail(loginDto);
return this.authService.loginWithPassword(loginDto); default:
case 'phone': throw new BadRequestException('服务器错误');
return this.authService.loginWithPhone(loginDto);
case 'email':
return this.authService.loginWithEmail(loginDto);
default:
throw new BadRequestException('服务器错误');
}
} }
}
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'))
@Post('logout') @Post('logout')
async logout(@Request() req) { async logout(@Request() req) {
const { userId, sessionId } = req.user; const { userId, sessionId } = req.user;
await this.userSessionService.invalidateSession(userId, sessionId); await this.userSessionService.invalidateSession(userId, sessionId);
return true; return true;
} }
} }

View File

@@ -25,21 +25,12 @@ import { OptionalAuthGuard } from './strategies/OptionalAuthGuard';
signOptions: { signOptions: {
expiresIn: configService.get<string>('JWT_EXPIRES_IN', '1d'), expiresIn: configService.get<string>('JWT_EXPIRES_IN', '1d'),
}, },
}) }),
}), }),
VerificationModule, VerificationModule,
], ],
controllers: [AuthController], controllers: [AuthController],
providers: [ providers: [AuthService, JwtStrategy, OptionalAuthGuard],
AuthService, exports: [PassportModule, JwtStrategy, AuthService, OptionalAuthGuard],
JwtStrategy,
OptionalAuthGuard,
],
exports: [
PassportModule,
JwtStrategy,
AuthService,
OptionalAuthGuard,
]
}) })
export class AuthModule { } export class AuthModule {}

View File

@@ -10,136 +10,146 @@ import { VerificationService } from 'src/verification/verification.service';
@Injectable() @Injectable()
export class AuthService { export class AuthService {
constructor(
private readonly userService: UserService,
private readonly jwtService: JwtService,
private readonly userSessionService: UserSessionService,
private readonly verificationService: VerificationService,
) {}
constructor( async loginWithPassword(loginDto: LoginDto) {
private readonly userService: UserService, const { account, password } = loginDto;
private readonly jwtService: JwtService, // 依次使用邮箱登录、手机号、账号
private readonly userSessionService: UserSessionService, const user = await this.userService.findOne(
private readonly verificationService: VerificationService, [{ email: account }, { phone: account }, { username: account }],
) { } {
withDeleted: true,
},
);
async loginWithPassword(loginDto: LoginDto) { if (user && user.deletedAt !== null) {
const { account, password } = loginDto; throw new BadRequestException('该账号注销中');
// 依次使用邮箱登录、手机号、账号
const user = await this.userService.findOne([
{ email: account },
{ phone: account },
{ username: account },
], {
withDeleted: true,
});
if (user && user.deletedAt !== null) {
throw new BadRequestException('该账号注销中');
}
if (user === null || !user.password_hash || !user.salt) {
throw new BadRequestException('账户或密码错误');
}
// 判断密码是否正确
const hashedPassword = this.hashPassword(password, user.salt);
if (hashedPassword !== user.password_hash) {
throw new BadRequestException('账户或密码错误');
}
// 登录成功颁发token
return {
token: await this.generateToken(user),
}
} }
async loginWithPhone(loginDto: LoginDto) { if (user === null || !user.password_hash || !user.salt) {
const { phone, code } = loginDto; throw new BadRequestException('账户或密码错误');
// 先判断验证码是否正确
const isValid = this.verificationService.verifyPhoneCode(phone, code, 'login');
switch (isValid) {
case 0:
break;
case -1:
throw new BadRequestException('验证码已过期');
case -2:
throw new BadRequestException('验证码错误');
case -3:
throw new BadRequestException('验证码已失效');
default:
throw new BadRequestException('验证码错误');
}
// 判断用户是否存在,若不存在则进行注册
let user = await this.userService.findOne({ phone }, { withDeleted: true });
if (user && user.deletedAt !== null) {
throw new BadRequestException('该账号注销中,请使用其他手机号');
}
if (!user) {
// 执行注册操作
user = await this.userService.create({ phone: phone });
}
if (!user || !user.userId) {// 注册失败或用户信息错误
throw new BadRequestException('请求失败,请稍后再试');
}
// 登录颁发token
return {
token: await this.generateToken(user),
}
} }
async loginWithEmail(loginDto: LoginDto) { // 判断密码是否正确
const { email, code } = loginDto; const hashedPassword = this.hashPassword(password, user.salt);
// 先判断验证码是否正确 if (hashedPassword !== user.password_hash) {
const isValid = this.verificationService.verifyEmailCode(email, code, 'login'); throw new BadRequestException('账户或密码错误');
switch (isValid) {
case 0:
break;
case -1:
throw new BadRequestException('验证码已过期,请重新获取');
case -2:
throw new BadRequestException('验证码错误');
case -3:
throw new BadRequestException('验证码已失效,请重新获取');
default:
throw new BadRequestException('验证码错误,请稍后再试');
}
// 判断用户是否存在,若不存在则进行注册
let user = await this.userService.findOne({ email }, { withDeleted: true });
if (user && user.deletedAt !== null) {
throw new BadRequestException('该账号注销中,请使用其他邮箱');
}
if (!user) {
// 执行注册操作
user = await this.userService.create({ email: email });
}
if (!user || !user.userId) {// 注册失败或用户信息错误
throw new BadRequestException('请求失败,请稍后再试');
}
// 登录颁发token
return {
token: await this.generateToken(user),
}
} }
private hashPassword(password: string, salt: string): string { // 登录成功颁发token
return createHash('sha256').update(`${password}${salt}`).digest('hex'); return {
token: await this.generateToken(user),
};
}
async loginWithPhone(loginDto: LoginDto) {
const { phone, code } = loginDto;
// 先判断验证码是否正确
const isValid = this.verificationService.verifyPhoneCode(
phone,
code,
'login',
);
switch (isValid) {
case 0:
break;
case -1:
throw new BadRequestException('验证码已过期');
case -2:
throw new BadRequestException('验证码错误');
case -3:
throw new BadRequestException('验证码已失效');
default:
throw new BadRequestException('验证码错误');
} }
private async generateToken(user: User) { // 判断用户是否存在,若不存在则进行注册
const payload = { let user = await this.userService.findOne({ phone }, { withDeleted: true });
userId: user.userId, if (user && user.deletedAt !== null) {
sessionId: uuidv4(), throw new BadRequestException('该账号注销中,请使用其他手机号');
}
// 存储
await this.userSessionService.createSession(payload.userId, payload.sessionId);
// 颁发token
return this.jwtService.sign(payload);
} }
} if (!user) {
// 执行注册操作
user = await this.userService.create({ phone: phone });
}
if (!user || !user.userId) {
// 注册失败或用户信息错误
throw new BadRequestException('请求失败,请稍后再试');
}
// 登录颁发token
return {
token: await this.generateToken(user),
};
}
async loginWithEmail(loginDto: LoginDto) {
const { email, code } = loginDto;
// 先判断验证码是否正确
const isValid = this.verificationService.verifyEmailCode(
email,
code,
'login',
);
switch (isValid) {
case 0:
break;
case -1:
throw new BadRequestException('验证码已过期,请重新获取');
case -2:
throw new BadRequestException('验证码错误');
case -3:
throw new BadRequestException('验证码已失效,请重新获取');
default:
throw new BadRequestException('验证码错误,请稍后再试');
}
// 判断用户是否存在,若不存在则进行注册
let user = await this.userService.findOne({ email }, { withDeleted: true });
if (user && user.deletedAt !== null) {
throw new BadRequestException('该账号注销中,请使用其他邮箱');
}
if (!user) {
// 执行注册操作
user = await this.userService.create({ email: email });
}
if (!user || !user.userId) {
// 注册失败或用户信息错误
throw new BadRequestException('请求失败,请稍后再试');
}
// 登录颁发token
return {
token: await this.generateToken(user),
};
}
private hashPassword(password: string, salt: string): string {
return createHash('sha256').update(`${password}${salt}`).digest('hex');
}
private async generateToken(user: User) {
const payload = {
userId: user.userId,
sessionId: uuidv4(),
};
// 存储
await this.userSessionService.createSession(
payload.userId,
payload.sessionId,
);
// 颁发token
return this.jwtService.sign(payload);
}
}

View File

@@ -1,31 +1,31 @@
import { IsEnum, IsString, Length, ValidateIf } from 'class-validator'; import { IsEnum, IsString, Length, ValidateIf } from 'class-validator';
export class LoginDto { export class LoginDto {
@IsEnum(['password', 'phone', 'email'], { message: '请求类型错误' }) @IsEnum(['password', 'phone', 'email'], { message: '请求类型错误' })
type: 'password' | 'phone' | 'email'; type: 'password' | 'phone' | 'email';
@ValidateIf(o => o.type === 'password') @ValidateIf((o) => o.type === 'password')
@IsString({ message: '账户必须输入' }) @IsString({ message: '账户必须输入' })
@Length(1, 254, { message: '账户异常' })// 用户名、邮箱、手机号 @Length(1, 254, { message: '账户异常' }) // 用户名、邮箱、手机号
account?: string; account?: string;
@ValidateIf(o => o.type === 'password') @ValidateIf((o) => o.type === 'password')
@IsString({ message: '密码必须输入' }) @IsString({ message: '密码必须输入' })
@Length(6, 32, { message: '密码异常' })// 6-32位 @Length(6, 32, { message: '密码异常' }) // 6-32位
password?: string; password?: string;
@ValidateIf(o => o.type === 'phone') @ValidateIf((o) => o.type === 'phone')
@IsString({ message: '手机号必须输入' }) @IsString({ message: '手机号必须输入' })
@Length(11, 11, { message: '手机号异常' })// 中国大陆11位数字 @Length(11, 11, { message: '手机号异常' }) // 中国大陆11位数字
phone?: string; phone?: string;
@ValidateIf(o => o.type === 'email') @ValidateIf((o) => o.type === 'email')
@IsString({ message: '邮箱必须输入' }) @IsString({ message: '邮箱必须输入' })
@Length(6, 254, { message: '邮箱异常' })// RFC 5321 @Length(6, 254, { message: '邮箱异常' }) // RFC 5321
email?: string; email?: string;
@ValidateIf(o => o.type === 'phone' || o.type === 'email') @ValidateIf((o) => o.type === 'phone' || o.type === 'email')
@IsString({ message: '验证码必须输入' }) @IsString({ message: '验证码必须输入' })
@Length(6, 6, { message: '验证码异常' })// 6位数字 @Length(6, 6, { message: '验证码异常' }) // 6位数字
code?: string; code?: string;
} }

View File

@@ -1,22 +1,28 @@
import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common"; import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { AuthGuard } from "@nestjs/passport"; import { AuthGuard } from '@nestjs/passport';
import { Observable, retry } from "rxjs";
@Injectable() @Injectable()
export class OptionalAuthGuard extends AuthGuard('jwt') implements CanActivate { export class OptionalAuthGuard extends AuthGuard('jwt') implements CanActivate {
async canActivate(context: ExecutionContext): Promise<boolean> { async canActivate(context: ExecutionContext): Promise<boolean> {
try { try {
await super.canActivate(context); await super.canActivate(context);
return true; return true;
} catch (error) { } catch (error) {
return true;// 如果验证失败,仍然允许访问 console.error('OptionalAuthGuard error:', error);
} return true; // 如果验证失败,仍然允许访问
} }
}
handleRequest<TUser = any>(err: any, user: any, info: any, context: ExecutionContext, status?: any): TUser { handleRequest<TUser = any>(
if (err || !user) { err: any,
return null; // 如果没有用户信息返回null user: any,
} // info: any,
return user; // 如果有用户信息,返回用户对象 // context: ExecutionContext,
// status?: any,
): TUser {
if (err || !user) {
return null; // 如果没有用户信息返回null
} }
} return user; // 如果有用户信息,返回用户对象
}
}

View File

@@ -1,33 +1,36 @@
import { Injectable, UnauthorizedException } from "@nestjs/common"; import { Injectable, UnauthorizedException } from '@nestjs/common';
import { ConfigService } from "@nestjs/config"; import { ConfigService } from '@nestjs/config';
import { PassportStrategy } from "@nestjs/passport"; import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from "passport-jwt"; import { ExtractJwt, Strategy } from 'passport-jwt';
import { UserSessionService } from "src/user/services/user-session.service"; import { UserSessionService } from 'src/user/services/user-session.service';
@Injectable() @Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') { export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
constructor( constructor(
private readonly userSessionService: UserSessionService, private readonly userSessionService: UserSessionService,
private readonly configService: ConfigService, private readonly configService: ConfigService,
) { ) {
super({ super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false, ignoreExpiration: false,
secretOrKey: configService.get<string>('JWT_SECRET', 'tone-page'), secretOrKey: configService.get<string>('JWT_SECRET', 'tone-page'),
}) });
}
async validate(payload: any) {
const { userId, sessionId } = payload ?? {};
const isValidSession = await this.userSessionService.isSessionValid(
userId,
sessionId,
);
if (!isValidSession) {
throw new UnauthorizedException('登录凭证已过期,请重新登录');
} }
async validate(payload: any) { return {
const { userId, sessionId } = payload ?? {}; userId,
sessionId,
const isValidSession = await this.userSessionService.isSessionValid(userId, sessionId); };
if (!isValidSession) { }
throw new UnauthorizedException('登录凭证已过期,请重新登录'); }
}
return {
userId,
sessionId,
}
}
}

View File

@@ -1,4 +1,14 @@
import { BadRequestException, Body, Controller, Get, Param, ParseUUIDPipe, Post, Req, Request, UseGuards } from '@nestjs/common'; import {
BadRequestException,
Body,
Controller,
Get,
Param,
ParseUUIDPipe,
Post,
Request,
UseGuards,
} from '@nestjs/common';
import { BlogService } from './blog.service'; import { BlogService } from './blog.service';
import { OptionalAuthGuard } from 'src/auth/strategies/OptionalAuthGuard'; import { OptionalAuthGuard } from 'src/auth/strategies/OptionalAuthGuard';
import { UserService } from 'src/user/user.service'; import { UserService } from 'src/user/user.service';
@@ -6,86 +16,91 @@ import { createBlogCommentDto } from './dto/create.blogcomment.dto';
@Controller('blog') @Controller('blog')
export class BlogController { export class BlogController {
constructor(
private readonly blogService: BlogService,
private readonly userService: UserService,
) {}
constructor( @Get()
private readonly blogService: BlogService, getBlogs() {
private readonly userService: UserService, return this.blogService.list();
) { } }
@Get() @Get(':id')
getBlogs() { async getBlog(@Param('id', new ParseUUIDPipe({ version: '4' })) id: string) {
return this.blogService.list(); const blog = await this.blogService.findById(id);
} if (!blog) throw new BadRequestException('文章不存在');
@Get(':id') const blogDataRes = await fetch(`${blog.contentUrl}`);
async getBlog( const blogContent = await blogDataRes.text();
@Param('id', new ParseUUIDPipe({ version: '4' })) id: string,
) {
const blog = await this.blogService.findById(id);
if (!blog) throw new BadRequestException('文章不存在');
const blogDataRes = await fetch(`${blog.contentUrl}`); await this.blogService.incrementViewCount(id);
const blogContent = await blogDataRes.text(); return {
id: blog.id,
title: blog.title,
createdAt: blog.createdAt,
content: blogContent,
};
}
await this.blogService.incrementViewCount(id); @Get(':id/comments')
return { async getBlogComments(
id: blog.id, @Param('id', new ParseUUIDPipe({ version: '4' })) id: string,
title: blog.title, ) {
createdAt: blog.createdAt, const blog = await this.blogService.findById(id);
content: blogContent, if (!blog) throw new BadRequestException('文章不存在');
};
}
@Get(':id/comments') return await this.blogService.getComments(id);
async getBlogComments( }
@Param('id', new ParseUUIDPipe({ version: '4' })) id: string,
) {
const blog = await this.blogService.findById(id);
if (!blog) throw new BadRequestException('文章不存在');
return await this.blogService.getComments(id); // 该接口允许匿名评论但仍需验证userId合法性
} @UseGuards(OptionalAuthGuard)
@Post(':id/comment')
async createBlogComment(
@Param('id', new ParseUUIDPipe({ version: '4' })) id: string,
@Body() commentData: createBlogCommentDto,
@Request() req,
) {
const { userId } = req.user || {};
const blog = await this.blogService.findById(id);
if (!blog) throw new BadRequestException('文章不存在');
// 该接口允许匿名评论但仍需验证userId合法性 const user = userId ? await this.userService.findOne({ userId }) : null;
@UseGuards(OptionalAuthGuard)
@Post(':id/comment')
async createBlogComment(
@Param('id', new ParseUUIDPipe({ version: '4' })) id: string,
@Body() commentData: createBlogCommentDto,
@Request() req,
) {
const { userId } = req.user || {};
const blog = await this.blogService.findById(id);
if (!blog) throw new BadRequestException('文章不存在');
let user = userId ? await this.userService.findOne({ userId }) : null; // 获取IP归属地
const ip =
// 获取IP归属地 req.ip ||
const ip = req.ip || req.socket.remoteAddress || req.headers['x-forwarded-for'] || '未知'; req.socket.remoteAddress ||
let address = '未知'; req.headers['x-forwarded-for'] ||
if (!['::1'].includes(ip)) { '未知';
const addressRes = await (await fetch(`https://mesh.if.iqiyi.com/aid/ip/info?version=1.1.1&ip=${ip}`)).json(); let address = '未知';
if (addressRes?.code == 0) { if (!['::1'].includes(ip)) {
const country: string = addressRes?.data?.countryCN || '未知'; const addressRes = await (
const province: string = addressRes?.data?.provinceCN || '中国'; await fetch(
if (country !== '中国') { `https://mesh.if.iqiyi.com/aid/ip/info?version=1.1.1&ip=${ip}`,
// 非中国,显示国家 )
address = country; ).json();
} else { if (addressRes?.code == 0) {
// 中国,显示省份 const country: string = addressRes?.data?.countryCN || '未知';
address = province; const province: string = addressRes?.data?.provinceCN || '中国';
} if (country !== '中国') {
} // 非中国,显示国家
address = country;
} else {
// 中国,显示省份
address = province;
} }
}
const comment = {
...commentData,
blogId: id,
user: user,
ip: ip,
address: address,
};
return await this.blogService.createComment(comment);
} }
const comment = {
...commentData,
blogId: id,
user: user,
ip: ip,
address: address,
};
return await this.blogService.createComment(comment);
}
} }

View File

@@ -8,9 +8,13 @@ import { AuthModule } from 'src/auth/auth.module';
import { UserModule } from 'src/user/user.module'; import { UserModule } from 'src/user/user.module';
@Module({ @Module({
imports: [TypeOrmModule.forFeature([Blog, BlogComment]), AuthModule, UserModule], imports: [
TypeOrmModule.forFeature([Blog, BlogComment]),
AuthModule,
UserModule,
],
controllers: [BlogController], controllers: [BlogController],
providers: [BlogService], providers: [BlogService],
exports: [BlogService], exports: [BlogService],
}) })
export class BlogModule { } export class BlogModule {}

View File

@@ -3,63 +3,61 @@ import { InjectRepository } from '@nestjs/typeorm';
import { Blog } from './entity/Blog.entity'; import { Blog } from './entity/Blog.entity';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
import { BlogComment } from './entity/BlogComment'; import { BlogComment } from './entity/BlogComment';
import { UserService } from 'src/user/user.service';
@Injectable() @Injectable()
export class BlogService { export class BlogService {
constructor(
@InjectRepository(Blog)
private readonly blogRepository: Repository<Blog>,
@InjectRepository(BlogComment)
private readonly blogCommentRepository: Repository<BlogComment>,
) {}
constructor( async list() {
@InjectRepository(Blog) return this.blogRepository.find({
private readonly blogRepository: Repository<Blog>, where: { deletedAt: null },
@InjectRepository(BlogComment) order: {
private readonly blogCommentRepository: Repository<BlogComment>, createdAt: 'DESC',
) { } },
});
}
async list() { async create(blog: Partial<Blog>) {
return this.blogRepository.find({ const newBlog = this.blogRepository.create(blog);
where: { deletedAt: null }, return this.blogRepository.save(newBlog);
order: { }
createdAt: 'DESC',
}
})
}
async create(blog: Partial<Blog>) { async update(id: string, blog: Partial<Blog>) {
const newBlog = this.blogRepository.create(blog); await this.blogRepository.update(id, blog);
return this.blogRepository.save(newBlog); return this.blogRepository.findOneBy({ id });
} }
async update(id: string, blog: Partial<Blog>) { async remove(id: string) {
await this.blogRepository.update(id, blog); const blog = await this.blogRepository.findOneBy({ id });
return this.blogRepository.findOneBy({ id }); if (!blog) return null;
} return this.blogRepository.softRemove(blog);
}
async remove(id: string) { async findById(id: string) {
const blog = await this.blogRepository.findOneBy({ id }); return this.blogRepository.findOneBy({ id });
if (!blog) return null; }
return this.blogRepository.softRemove(blog);
}
async findById(id: string) { async incrementViewCount(id: string) {
return this.blogRepository.findOneBy({ id }); await this.blogRepository.increment({ id }, 'viewCount', 1);
} }
async incrementViewCount(id: string) { async getComments(id: string) {
await this.blogRepository.increment({ id }, 'viewCount', 1); return this.blogCommentRepository.find({
} where: { blogId: id },
relations: ['user'],
order: {
createdAt: 'DESC',
},
});
}
async getComments(id: string) { async createComment(comment: Partial<BlogComment>) {
return this.blogCommentRepository.find({ const newComment = this.blogCommentRepository.create(comment);
where: { blogId: id }, return this.blogCommentRepository.save(newComment);
relations: ['user'], }
order: {
createdAt: 'DESC',
}
});
}
async createComment(comment: Partial<BlogComment>) {
const newComment = this.blogCommentRepository.create(comment);
return this.blogCommentRepository.save(newComment);
}
} }

View File

@@ -1,10 +1,10 @@
import { IsOptional, IsString, IsUUID } from "class-validator"; import { IsOptional, IsString, IsUUID } from 'class-validator';
export class createBlogCommentDto { export class createBlogCommentDto {
@IsString({ message: '评论内容不能为空' }) @IsString({ message: '评论内容不能为空' })
content: string; content: string;
@IsOptional() @IsOptional()
@IsUUID('4', { message: '父评论ID格式错误' }) @IsUUID('4', { message: '父评论ID格式错误' })
parentId?: string; parentId?: string;
} }

View File

@@ -1,35 +1,43 @@
import { Column, CreateDateColumn, DeleteDateColumn, Entity, OneToMany, PrimaryGeneratedColumn, UpdateDateColumn } from "typeorm"; import {
import { BlogComment } from "./BlogComment"; Column,
CreateDateColumn,
DeleteDateColumn,
Entity,
OneToMany,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import { BlogComment } from './BlogComment';
@Entity() @Entity()
export class Blog { export class Blog {
@PrimaryGeneratedColumn('uuid') @PrimaryGeneratedColumn('uuid')
id: string; id: string;
@Column() @Column()
title: string; title: string;
@Column() @Column()
description: string; description: string;
@Column() @Column()
contentUrl: string; contentUrl: string;
@Column({ default: 0 }) @Column({ default: 0 })
viewCount: number; viewCount: number;
@CreateDateColumn({ precision: 3 }) @CreateDateColumn({ precision: 3 })
createdAt: Date; createdAt: Date;
@UpdateDateColumn({ precision: 3 }) @UpdateDateColumn({ precision: 3 })
updatedAt: Date; updatedAt: Date;
@DeleteDateColumn({ precision: 3, nullable: true }) @DeleteDateColumn({ precision: 3, nullable: true })
deletedAt: Date; deletedAt: Date;
// 权限关系 TODO // 权限关系 TODO
// 关系 // 关系
@OneToMany(() => BlogComment, blog => blog.id) @OneToMany(() => BlogComment, (blog) => blog.id)
comments: BlogComment[]; comments: BlogComment[];
} }

View File

@@ -1,33 +1,41 @@
import { User } from "src/user/entities/user.entity"; import { User } from 'src/user/entities/user.entity';
import { Column, CreateDateColumn, DeleteDateColumn, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from "typeorm"; import {
Column,
CreateDateColumn,
DeleteDateColumn,
Entity,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
} from 'typeorm';
@Entity() @Entity()
export class BlogComment { export class BlogComment {
@PrimaryGeneratedColumn('uuid') @PrimaryGeneratedColumn('uuid')
id: string; id: string;
@Column() @Column()
content: string; content: string;
@Column() @Column()
ip: string; ip: string;
@Column() @Column()
address: string; address: string;
@CreateDateColumn({ precision: 3 }) @CreateDateColumn({ precision: 3 })
createdAt: Date; createdAt: Date;
@DeleteDateColumn({ precision: 3, nullable: true }) @DeleteDateColumn({ precision: 3, nullable: true })
deletedAt: Date; deletedAt: Date;
@ManyToOne(() => User, { nullable: true }) @ManyToOne(() => User, { nullable: true })
@JoinColumn({ name: 'userId' }) @JoinColumn({ name: 'userId' })
user: User | null; user: User | null;
@Column({ type: 'uuid', nullable: true }) @Column({ type: 'uuid', nullable: true })
blogId: string | null; blogId: string | null;
@Column({ type: 'uuid', nullable: true }) @Column({ type: 'uuid', nullable: true })
parentId: string | null; parentId: string | null;
} }

View File

@@ -1,3 +1,4 @@
import { SetMetadata } from "@nestjs/common"; import { SetMetadata } from '@nestjs/common';
export const Permissions = (...permissions: string[]) => SetMetadata('permissions', permissions); export const Permissions = (...permissions: string[]) =>
SetMetadata('permissions', permissions);

View File

@@ -1,3 +1,3 @@
import { SetMetadata } from "@nestjs/common"; import { SetMetadata } from '@nestjs/common';
export const Roles = (...roles: string[]) => SetMetadata('roles', roles); export const Roles = (...roles: string[]) => SetMetadata('roles', roles);

View File

@@ -1,40 +1,47 @@
import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common"; import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core'; import { Reflector } from '@nestjs/core';
import { PermissionService } from "src/role/services/permission.service"; import { PermissionService } from 'src/role/services/permission.service';
import { RolePermissionService } from "src/role/services/role-permission.service"; import { RolePermissionService } from 'src/role/services/role-permission.service';
import { UserRoleService } from "src/role/services/user-role.service"; import { UserRoleService } from 'src/role/services/user-role.service';
@Injectable() @Injectable()
export class PermissionGuard implements CanActivate { export class PermissionGuard implements CanActivate {
constructor( constructor(
private reflector: Reflector, private reflector: Reflector,
private readonly userRoleService: UserRoleService, private readonly userRoleService: UserRoleService,
private readonly rolePermissionService: RolePermissionService, private readonly rolePermissionService: RolePermissionService,
private readonly permissionService: PermissionService, private readonly permissionService: PermissionService,
) { } ) {}
async canActivate(context: ExecutionContext): Promise<boolean> { async canActivate(context: ExecutionContext): Promise<boolean> {
const requiredPermissions = this.reflector.getAllAndOverride<string[]>('permissions', [ const requiredPermissions = this.reflector.getAllAndOverride<string[]>(
context.getHandler(), 'permissions',
context.getClass(), [context.getHandler(), context.getClass()],
]); );
if (!requiredPermissions) return true; if (!requiredPermissions) return true;
const request = context.switchToHttp().getRequest(); const request = context.switchToHttp().getRequest();
const userId = request.user?.userId; const userId = request.user?.userId;
if (!userId) return false; if (!userId) return false;
// 查询用户拥有的有效角色ID // 查询用户拥有的有效角色ID
const userRoleIds = await this.userRoleService.findValidRoleIdsByUserId(userId); const userRoleIds =
await this.userRoleService.findValidRoleIdsByUserId(userId);
// 查询用户拥有的有效角色ID对应的权限ID // 查询用户拥有的有效角色ID对应的权限ID
const userPermissionIds = await this.rolePermissionService.findPermissionIdsByRoleIds(userRoleIds); const userPermissionIds =
await this.rolePermissionService.findPermissionIdsByRoleIds(userRoleIds);
// 查询用户拥有的权限ID对应的权限名 // 查询用户拥有的权限ID对应的权限名
const userPermissionNames = await this.permissionService.findPermissionNamesByPermissionIds(userPermissionIds); const userPermissionNames =
await this.permissionService.findPermissionNamesByPermissionIds(
userPermissionIds,
);
return requiredPermissions.every(permission => userPermissionNames.includes(permission)) return requiredPermissions.every((permission) =>
} userPermissionNames.includes(permission),
} );
}
}

View File

@@ -1,36 +1,37 @@
import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common"; import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core'; import { Reflector } from '@nestjs/core';
import { RoleService } from "src/role/services/role.service"; import { RoleService } from 'src/role/services/role.service';
import { UserRoleService } from "src/role/services/user-role.service"; import { UserRoleService } from 'src/role/services/user-role.service';
@Injectable() @Injectable()
export class RolesGuard implements CanActivate { export class RolesGuard implements CanActivate {
constructor( constructor(
private reflector: Reflector, private reflector: Reflector,
private readonly userRoleService: UserRoleService, private readonly userRoleService: UserRoleService,
private readonly roleService: RoleService, private readonly roleService: RoleService,
) { } ) {}
async canActivate(context: ExecutionContext): Promise<boolean> { async canActivate(context: ExecutionContext): Promise<boolean> {
const requiredRoles = this.reflector.getAllAndOverride<string[]>('roles', [ const requiredRoles = this.reflector.getAllAndOverride<string[]>('roles', [
context.getHandler(), context.getHandler(),
context.getClass(), context.getClass(),
]); ]);
if (!requiredRoles) return true; if (!requiredRoles) return true;
const request = context.switchToHttp().getRequest(); const request = context.switchToHttp().getRequest();
const userId = request.user?.userId; const userId = request.user?.userId;
if (!userId) return false; if (!userId) return false;
// 查询用户拥有的有效角色Id // 查询用户拥有的有效角色Id
const userRoleIds = await this.userRoleService.findValidRoleIdsByUserId(userId); const userRoleIds =
await this.userRoleService.findValidRoleIdsByUserId(userId);
// 查询用户角色Id对应的角色名 // 查询用户角色Id对应的角色名
const userRoleNames = await this.roleService.findRoleNamesByRoleIds(userRoleIds); const userRoleNames =
await this.roleService.findRoleNamesByRoleIds(userRoleIds);
return requiredRoles.some(role => userRoleNames.includes(role)); return requiredRoles.some((role) => userRoleNames.includes(role));
} }
} }

View File

@@ -1,16 +1,24 @@
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from "@nestjs/common"; import {
import { Observable } from "rxjs"; CallHandler,
import { map } from "rxjs/operators"; ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable() @Injectable()
export class ResponseInterceptor implements NestInterceptor { export class ResponseInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler<any>): Observable<any> | Promise<Observable<any>> { intercept(
return next.handle().pipe( context: ExecutionContext,
map(data => ({ next: CallHandler<any>,
statusCode: 200, ): Observable<any> | Promise<Observable<any>> {
message: '请求成功', return next.handle().pipe(
data, map((data) => ({
})), statusCode: 200,
); message: '请求成功',
} data,
} })),
);
}
}

View File

@@ -5,22 +5,26 @@ import { ResponseInterceptor } from './common/interceptors/response.interceptor'
async function bootstrap() { async function bootstrap() {
const app = await NestFactory.create(AppModule); const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({ app.useGlobalPipes(
transform: true, new ValidationPipe({
whitelist: true, transform: true,
forbidNonWhitelisted: true, whitelist: true,
stopAtFirstError: true, forbidNonWhitelisted: true,
exceptionFactory: (errors) => { stopAtFirstError: true,
const error = errors[0]; exceptionFactory: (errors) => {
const firstConstraint = error.constraints ? Object.values(error.constraints)[0] : '验证失败'; const error = errors[0];
const firstConstraint = error.constraints
? Object.values(error.constraints)[0]
: '验证失败';
throw new BadRequestException({ throw new BadRequestException({
message: firstConstraint, message: firstConstraint,
error: 'Bad Request', error: 'Bad Request',
statusCode: 400 statusCode: 400,
}); });
} },
})); }),
);
app.useGlobalInterceptors(new ResponseInterceptor()); app.useGlobalInterceptors(new ResponseInterceptor());
await app.listen(process.env.PORT ?? 3001); await app.listen(process.env.PORT ?? 3001);
} }

View File

@@ -2,37 +2,40 @@ import { Injectable } from '@nestjs/common';
@Injectable() @Injectable()
export class NotificationService { export class NotificationService {
sendEmail(email: string, subject: string, content: string) {
throw new Error(
`Email sending is not implemented yet. Email: ${email}, Subject: ${subject}, Content: ${content}`,
);
}
sendEmail(email: string, subject: string, content: string) { /**
* @deprecated 短信签名暂未通过
} */
async sendSMS(phone: string, type: 'login', code: string) {
/** throw new Error(
* @deprecated 短信签名暂未通过 `SMS sending is not implemented yet. Phone: ${phone}, Type: ${type}, Code: ${code}`,
*/ );
async sendSMS(phone: string, type: 'login', code: string) { // const config = new $OpenApi.Config({
// const config = new $OpenApi.Config({ // accessKeyId: process.env.ALIYUN_ACCESS_KEY_ID,
// accessKeyId: process.env.ALIYUN_ACCESS_KEY_ID, // accessKeySecret: process.env.ALIYUN_ACCESS_KEY_SECRET,
// accessKeySecret: process.env.ALIYUN_ACCESS_KEY_SECRET, // })
// }) // config.endpoint = 'dysmsapi.aliyuncs.com';
// config.endpoint = 'dysmsapi.aliyuncs.com'; // const client = new Client(config);
// const client = new Client(config); // const request = new $dysmsapi.SendSmsRequest({});
// const request = new $dysmsapi.SendSmsRequest({}); // request.phoneNumbers = phone;
// request.phoneNumbers = phone; // request.signName = (() => {
// request.signName = (() => { // switch (type) {
// switch (type) { // case 'login':
// case 'login': // return process.env.ALIYUN_SMS_LOGIN_SIGN_NAME;
// return process.env.ALIYUN_SMS_LOGIN_SIGN_NAME; // default:
// default: // throw new Error('Unknown SMS type');
// throw new Error('Unknown SMS type'); // }
// } // })();
// })(); // request.templateCode = code;
// request.templateCode = code; // await client.sendSms(request).then(a => {
// console.log(a)
// await client.sendSms(request).then(a => { // }).catch(err => {
// console.log(a) // console.error(err);
// }).catch(err => { // })
// console.error(err); }
// })
}
} }

View File

@@ -4,18 +4,15 @@ import { AuthGuard } from '@nestjs/passport';
@Controller('oss') @Controller('oss')
export class OssController { export class OssController {
constructor(private readonly ossService: OssService) {}
constructor( @UseGuards(AuthGuard('jwt'))
private readonly ossService: OssService, @Get('sts')
) { } async getStsToken(@Request() req) {
const { userId } = req.user;
@UseGuards(AuthGuard('jwt')) return {
@Get('sts') ...(await this.ossService.getStsToken(`${userId}`)),
async getStsToken(@Request() req) { userId,
const { userId, sessionId } = req.user; };
return { }
...await this.ossService.getStsToken(`${userId}`),
userId,
}
}
} }

View File

@@ -4,6 +4,6 @@ import { OssController } from './oss.controller';
@Module({ @Module({
providers: [OssService], providers: [OssService],
controllers: [OssController] controllers: [OssController],
}) })
export class OssModule {} export class OssModule {}

View File

@@ -3,51 +3,51 @@ import { STS } from 'ali-oss';
@Injectable() @Injectable()
export class OssService { export class OssService {
private sts = new STS({
accessKeyId: process.env.ALIYUN_ACCESS_KEY_ID,
accessKeySecret: process.env.ALIYUN_ACCESS_KEY_SECRET,
});
private sts = new STS({ private stsCache: {
accessKeyId: process.env.ALIYUN_ACCESS_KEY_ID, [session: string]: {
accessKeySecret: process.env.ALIYUN_ACCESS_KEY_SECRET, credentials: {
}); AccessKeyId: string;
AccessKeySecret: string;
SecurityToken: string;
Expiration: string;
};
expireTime: number; // 时间戳,单位为毫秒
};
} = {};
private stsCache: { /** @todo 该方法存在缓存穿透问题,待优化 */
[session: string]: { async getStsToken(session: string) {
credentials: { if (this.stsCache[session]) {
AccessKeyId: string; const cached = this.stsCache[session];
AccessKeySecret: string; // 检查缓存是否过期
SecurityToken: string; if (cached.expireTime > Date.now()) {
Expiration: string; return cached.credentials;
}; } else {
expireTime: number; // 时间戳,单位为毫秒 // 如果过期,删除缓存
} delete this.stsCache[session];
} = {}; }
/** @todo 该方法存在缓存穿透问题,待优化 */
async getStsToken(session: string) {
if (this.stsCache[session]) {
const cached = this.stsCache[session];
// 检查缓存是否过期
if (cached.expireTime > Date.now()) {
return cached.credentials;
} else {
// 如果过期,删除缓存
delete this.stsCache[session];
}
}
return this.sts.assumeRole(
process.env.ALIYUN_OSS_STS_ROLE_ARN, ``, 3600, `${session}`,
).then((res) => {
// 缓存
this.stsCache[session] = {
credentials: res.credentials,
expireTime: new Date(res.credentials.Expiration).getTime() - 5 * 60 * 1000, // 提前5分钟过期,
};
return res.credentials;
}).catch(err => {
console.error('获取STS Token失败:', err);
throw new Error('获取STS Token失败');
})
} }
return this.sts
.assumeRole(process.env.ALIYUN_OSS_STS_ROLE_ARN, ``, 3600, `${session}`)
.then((res) => {
// 缓存
this.stsCache[session] = {
credentials: res.credentials,
expireTime:
new Date(res.credentials.Expiration).getTime() - 5 * 60 * 1000, // 提前5分钟过期,
};
return res.credentials;
})
.catch((err) => {
console.error('获取STS Token失败:', err);
throw new Error('获取STS Token失败');
});
}
} }

View File

@@ -1,34 +1,41 @@
import { Column, CreateDateColumn, Entity, Index, PrimaryGeneratedColumn, UpdateDateColumn } from "typeorm"; import {
Column,
CreateDateColumn,
Entity,
Index,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
type ResourceTag = { type ResourceTag = {
name: string; name: string;
type: string; type: string;
} };
@Entity() @Entity()
export class Resource { export class Resource {
@PrimaryGeneratedColumn('uuid') @PrimaryGeneratedColumn('uuid')
@Index() @Index()
id: string; id: string;
@Column() @Column()
title: string; title: string;
@Column() @Column()
description: string; description: string;
@Column() @Column()
imageUrl: string; imageUrl: string;
@Column() @Column()
link: string; link: string;
@Column('jsonb') @Column('jsonb')
tags: ResourceTag[]; tags: ResourceTag[];
@CreateDateColumn({ precision: 3 }) @CreateDateColumn({ precision: 3 })
createdAt: Date; createdAt: Date;
@UpdateDateColumn({ precision: 3 }) @UpdateDateColumn({ precision: 3 })
updatedAt: Date; updatedAt: Date;
} }

View File

@@ -3,13 +3,10 @@ import { ResourceService } from './resource.service';
@Controller('resource') @Controller('resource')
export class ResourceController { export class ResourceController {
constructor(private readonly resourceService: ResourceService) {}
constructor( @Get()
private readonly resourceService: ResourceService, async getResource() {
) { } return this.resourceService.findAll();
}
@Get()
async getResource() {
return this.resourceService.findAll();
}
} }

View File

@@ -5,7 +5,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { Resource } from './entity/resource.entity'; import { Resource } from './entity/resource.entity';
@Module({ @Module({
imports:[TypeOrmModule.forFeature([Resource])], imports: [TypeOrmModule.forFeature([Resource])],
controllers: [ResourceController], controllers: [ResourceController],
providers: [ResourceService], providers: [ResourceService],
exports: [ResourceService], exports: [ResourceService],

View File

@@ -5,34 +5,34 @@ import { InjectRepository } from '@nestjs/typeorm';
@Injectable() @Injectable()
export class ResourceService { export class ResourceService {
constructor( constructor(
@InjectRepository(Resource) @InjectRepository(Resource)
private readonly resourceRepository: Repository<Resource>, private readonly resourceRepository: Repository<Resource>,
) { } ) {}
async findAll(): Promise<Resource[]> { async findAll(): Promise<Resource[]> {
return this.resourceRepository.find({ return this.resourceRepository.find({
order: { order: {
createdAt: 'DESC', createdAt: 'DESC',
} },
}); });
} }
async findById(id: string): Promise<Resource> { async findById(id: string): Promise<Resource> {
return this.resourceRepository.findOne({ where: { id } }); return this.resourceRepository.findOne({ where: { id } });
} }
async create(data: Partial<Resource>): Promise<Resource> { async create(data: Partial<Resource>): Promise<Resource> {
const resource = this.resourceRepository.create(data); const resource = this.resourceRepository.create(data);
return this.resourceRepository.save(resource); return this.resourceRepository.save(resource);
} }
async update(id: string, data: Partial<Resource>): Promise<Resource> { async update(id: string, data: Partial<Resource>): Promise<Resource> {
await this.resourceRepository.update(id, data); await this.resourceRepository.update(id, data);
return this.resourceRepository.findOne({ where: { id } }); return this.resourceRepository.findOne({ where: { id } });
} }
async delete(id: string): Promise<void> { async delete(id: string): Promise<void> {
await this.resourceRepository.delete(id); await this.resourceRepository.delete(id);
} }
} }

View File

@@ -1,13 +1,13 @@
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm"; import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity() @Entity()
export class Permission { export class Permission {
@PrimaryGeneratedColumn('uuid') @PrimaryGeneratedColumn('uuid')
id: string; id: string;
@Column({ unique: true }) @Column({ unique: true })
name: string; name: string;
@Column() @Column()
description: string; description: string;
} }

View File

@@ -1,11 +1,11 @@
import { Entity, Index, PrimaryColumn } from "typeorm"; import { Entity, Index, PrimaryColumn } from 'typeorm';
@Entity() @Entity()
@Index(['roleId', 'permissionId'], { unique: true }) @Index(['roleId', 'permissionId'], { unique: true })
export class RolePermission { export class RolePermission {
@PrimaryColumn('uuid') @PrimaryColumn('uuid')
roleId: string; roleId: string;
@PrimaryColumn('uuid') @PrimaryColumn('uuid')
permissionId: string; permissionId: string;
} }

View File

@@ -1,13 +1,13 @@
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm"; import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity() @Entity()
export class Role { export class Role {
@PrimaryGeneratedColumn('uuid') @PrimaryGeneratedColumn('uuid')
id: string; id: string;
@Column({ unique: true }) @Column({ unique: true })
name: string; name: string;
@Column() @Column()
localName: string; localName: string;
} }

View File

@@ -1,23 +1,29 @@
import { Column, CreateDateColumn, Entity, Index, PrimaryGeneratedColumn } from "typeorm"; import {
Column,
CreateDateColumn,
Entity,
Index,
PrimaryGeneratedColumn,
} from 'typeorm';
@Entity() @Entity()
@Index(['userId', 'roleId']) @Index(['userId', 'roleId'])
export class UserRole { export class UserRole {
@PrimaryGeneratedColumn('uuid') @PrimaryGeneratedColumn('uuid')
id: string; id: string;
@Column('uuid') @Column('uuid')
roleId: string; roleId: string;
@Column('uuid') @Column('uuid')
userId: string userId: string;
@Column() @Column()
isEnabled: boolean; isEnabled: boolean;
@CreateDateColumn({ precision: 3 }) @CreateDateColumn({ precision: 3 })
createdAt: Date; createdAt: Date;
@Column({ nullable: true, precision: 3 }) @Column({ nullable: true, precision: 3 })
expiredAt?: Date; expiredAt?: Date;
} }

View File

@@ -10,8 +10,20 @@ import { UserRole } from './entities/user-role.entity';
import { PermissionService } from './services/permission.service'; import { PermissionService } from './services/permission.service';
@Module({ @Module({
imports: [TypeOrmModule.forFeature([Role, Permission, RolePermission, UserRole])], imports: [
providers: [RolePermissionService, RoleService, UserRoleService, PermissionService], TypeOrmModule.forFeature([Role, Permission, RolePermission, UserRole]),
exports: [RolePermissionService, RoleService, UserRoleService, PermissionService], ],
providers: [
RolePermissionService,
RoleService,
UserRoleService,
PermissionService,
],
exports: [
RolePermissionService,
RoleService,
UserRoleService,
PermissionService,
],
}) })
export class RoleModule { } export class RoleModule {}

View File

@@ -1,51 +1,59 @@
import { BadRequestException, Injectable } from "@nestjs/common"; import { BadRequestException, Injectable } from '@nestjs/common';
import { InjectRepository } from "@nestjs/typeorm"; import { InjectRepository } from '@nestjs/typeorm';
import { Permission } from "../entities/permission.entity"; import { Permission } from '../entities/permission.entity';
import { In, Repository } from "typeorm"; import { In, Repository } from 'typeorm';
@Injectable() @Injectable()
export class PermissionService { export class PermissionService {
constructor(
@InjectRepository(Permission)
private readonly permissionRepository: Repository<Permission>,
) {}
constructor( async findPermissionNamesByPermissionIds(
@InjectRepository(Permission) permissionIds: string[],
private readonly permissionRepository: Repository<Permission>, ): Promise<string[]> {
) { } const permissions =
await this.findPermissionsByPermissionIds(permissionIds);
return permissions.map((permission) => permission.name);
}
async findPermissionNamesByPermissionIds(permissionIds: string[]): Promise<string[]> { async findPermissionsByPermissionIds(
const permissions = await this.findPermissionsByPermissionIds(permissionIds); permissionIds: string[],
return permissions.map(permission => permission.name); ): Promise<Permission[]> {
return this.permissionRepository.find({
where: {
id: In(permissionIds),
},
});
}
async findPermissionByIds(permissionIds: string[]): Promise<Permission[]> {
return this.permissionRepository.find({
where: {
id: In(permissionIds),
},
});
}
async list() {
return this.permissionRepository.find();
}
async create(
permission: Pick<Permission, 'name' | 'description'>,
): Promise<Permission> {
const newPermission = this.permissionRepository.create(permission);
return this.permissionRepository.save(newPermission);
}
async delete(permissionId: string): Promise<void> {
const existingPermission = await this.permissionRepository.findOne({
where: { id: permissionId },
});
if (!existingPermission) {
throw new BadRequestException('Permission not found');
} }
await this.permissionRepository.delete(existingPermission.id);
async findPermissionsByPermissionIds(permissionIds: string[]): Promise<Permission[]> { }
return this.permissionRepository.find({ }
where: {
id: In(permissionIds),
}
})
}
async findPermissionByIds(permissionIds: string[]): Promise<Permission[]> {
return this.permissionRepository.find({
where: {
id: In(permissionIds),
}
});
}
async list() {
return this.permissionRepository.find();
}
async create(permission: Pick<Permission, 'name' | 'description'>): Promise<Permission> {
const newPermission = this.permissionRepository.create(permission);
return this.permissionRepository.save(newPermission);
}
async delete(permissionId: string): Promise<void> {
const existingPermission = await this.permissionRepository.findOne({ where: { id: permissionId } });
if (!existingPermission) {
throw new BadRequestException('Permission not found');
}
await this.permissionRepository.delete(existingPermission.id);
}
}

View File

@@ -1,42 +1,47 @@
import { Injectable } from "@nestjs/common"; import { Injectable } from '@nestjs/common';
import { InjectRepository } from "@nestjs/typeorm"; import { InjectRepository } from '@nestjs/typeorm';
import { RolePermission } from "../entities/role-permission.entity"; import { RolePermission } from '../entities/role-permission.entity';
import { In, Repository } from "typeorm"; import { In, Repository } from 'typeorm';
@Injectable() @Injectable()
export class RolePermissionService { export class RolePermissionService {
constructor(
@InjectRepository(RolePermission)
private readonly rolePermissionRepository: Repository<RolePermission>,
) {}
constructor( async findPermissionIdsByRoleIds(roleIds: string[]): Promise<string[]> {
@InjectRepository(RolePermission) const rolePermissions = await this.rolePermissionRepository.find({
private readonly rolePermissionRepository: Repository<RolePermission>, where: {
) { } roleId: In(roleIds),
},
});
async findPermissionIdsByRoleIds(roleIds: string[]): Promise<string[]> { return rolePermissions.map((rp) => rp.permissionId);
const rolePermissions = await this.rolePermissionRepository.find({ }
where: {
roleId: In(roleIds),
}
});
return rolePermissions.map(rp => rp.permissionId); async addRolePermissions(
} roleId: string,
permissionIds: string[],
): Promise<void> {
const rolePermissions = permissionIds.map((permissionId) => {
const rolePermission = this.rolePermissionRepository.create({
roleId,
permissionId,
});
return rolePermission;
});
async addRolePermissions(roleId: string, permissionIds: string[]): Promise<void> { await this.rolePermissionRepository.save(rolePermissions);
const rolePermissions = permissionIds.map(permissionId => { }
const rolePermission = this.rolePermissionRepository.create({
roleId,
permissionId,
});
return rolePermission;
});
await this.rolePermissionRepository.save(rolePermissions); async deleteRolePermissions(
} roleId: string,
permissionIds: string[],
async deleteRolePermissions(roleId: string, permissionIds: string[]): Promise<void> { ): Promise<void> {
await this.rolePermissionRepository.delete({ await this.rolePermissionRepository.delete({
roleId, roleId,
permissionId: In(permissionIds), permissionId: In(permissionIds),
}); });
} }
} }

View File

@@ -1,43 +1,44 @@
import { BadRequestException, Injectable } from "@nestjs/common"; import { BadRequestException, Injectable } from '@nestjs/common';
import { InjectRepository } from "@nestjs/typeorm"; import { InjectRepository } from '@nestjs/typeorm';
import { Role } from "../entities/role.entity"; import { Role } from '../entities/role.entity';
import { In, Repository } from "typeorm"; import { In, Repository } from 'typeorm';
@Injectable() @Injectable()
export class RoleService { export class RoleService {
constructor(
@InjectRepository(Role)
private readonly roleRepository: Repository<Role>,
) {}
constructor( async findRoleNamesByRoleIds(roleIds: string[]): Promise<string[]> {
@InjectRepository(Role) const roles = await this.findRolesByRoleIds(roleIds);
private readonly roleRepository: Repository<Role>, return roles.map((role) => role.name);
) { } }
async findRoleNamesByRoleIds(roleIds: string[]): Promise<string[]> { async findRolesByRoleIds(roleIds: string[]): Promise<Role[]> {
const roles = await this.findRolesByRoleIds(roleIds); return this.roleRepository.find({
return roles.map(role => role.name); where: {
id: In(roleIds),
},
});
}
async create(role: Pick<Role, 'name' | 'localName'>): Promise<Role> {
const newRole = this.roleRepository.create(role);
return this.roleRepository.save(newRole);
}
async list(): Promise<Role[]> {
return this.roleRepository.find();
}
async delete(roleId: string): Promise<void> {
const existingRole = await this.roleRepository.findOne({
where: { id: roleId },
});
if (!existingRole) {
throw new BadRequestException('Role not found');
} }
await this.roleRepository.delete(existingRole.id);
async findRolesByRoleIds(roleIds: string[]): Promise<Role[]> { }
return this.roleRepository.find({ }
where: {
id: In(roleIds),
}
})
}
async create(role: Pick<Role, 'name' | 'localName'>): Promise<Role> {
const newRole = this.roleRepository.create(role);
return this.roleRepository.save(newRole);
}
async list(): Promise<Role[]> {
return this.roleRepository.find();
}
async delete(roleId: string): Promise<void> {
const existingRole = await this.roleRepository.findOne({ where: { id: roleId } });
if (!existingRole) {
throw new BadRequestException('Role not found');
}
await this.roleRepository.delete(existingRole.id);
}
}

View File

@@ -1,57 +1,59 @@
import { Injectable } from "@nestjs/common"; import { Injectable } from '@nestjs/common';
import { InjectRepository } from "@nestjs/typeorm"; import { InjectRepository } from '@nestjs/typeorm';
import { UserRole } from "src/role/entities/user-role.entity"; import { UserRole } from 'src/role/entities/user-role.entity';
import { IsNull, MoreThanOrEqual, Repository } from "typeorm"; import { IsNull, MoreThanOrEqual, Repository } from 'typeorm';
@Injectable() @Injectable()
export class UserRoleService { export class UserRoleService {
constructor( constructor(
@InjectRepository(UserRole) @InjectRepository(UserRole)
private readonly userRoleRepository: Repository<UserRole>, private readonly userRoleRepository: Repository<UserRole>,
) { } ) {}
async findRoleIdsByUserId(userId: string): Promise<string[]> { async findRoleIdsByUserId(userId: string): Promise<string[]> {
const userRoles = await this.userRoleRepository.find({ const userRoles = await this.userRoleRepository.find({
where: { where: {
userId, userId,
} },
}); });
return userRoles.map(ur => ur.roleId); return userRoles.map((ur) => ur.roleId);
} }
async findValidRoleIdsByUserId(userId: string): Promise<string[]> { async findValidRoleIdsByUserId(userId: string): Promise<string[]> {
return (await this.findValidRolesByUserId(userId)).map(ur => ur.roleId); return (await this.findValidRolesByUserId(userId)).map((ur) => ur.roleId);
} }
async findValidRolesByUserId(userId: string) { async findValidRolesByUserId(userId: string) {
const now = new Date(); const now = new Date();
return this.userRoleRepository.find({ return this.userRoleRepository.find({
where: [ where: [
{ {
userId, userId,
isEnabled: true, isEnabled: true,
expiredAt: MoreThanOrEqual(now), expiredAt: MoreThanOrEqual(now),
}, },
{ {
userId, userId,
isEnabled: true, isEnabled: true,
expiredAt: IsNull(), expiredAt: IsNull(),
} },
] ],
}) });
} }
async addUserRole(userRole: Pick<UserRole, 'roleId' | 'userId' | 'isEnabled' | 'expiredAt'>): Promise<void> { async addUserRole(
const newUserRole = this.userRoleRepository.create(userRole); userRole: Pick<UserRole, 'roleId' | 'userId' | 'isEnabled' | 'expiredAt'>,
await this.userRoleRepository.save(newUserRole); ): Promise<void> {
} const newUserRole = this.userRoleRepository.create(userRole);
await this.userRoleRepository.save(newUserRole);
}
async deleteUserRole(userId: string, roleId: string): Promise<void> { async deleteUserRole(userId: string, roleId: string): Promise<void> {
await this.userRoleRepository.delete({ await this.userRoleRepository.delete({
userId, userId,
roleId, roleId,
}); });
} }
} }

View File

@@ -1,10 +1,11 @@
import { IsString, Length, Matches } from "class-validator"; import { IsString, Length, Matches } from 'class-validator';
export class UpdateUserPasswordDto { export class UpdateUserPasswordDto {
@IsString({ message: '密码不得为空' }) @IsString({ message: '密码不得为空' })
@Length(6, 32, { message: '密码长度只能为6~32' }) @Length(6, 32, { message: '密码长度只能为6~32' })
@Matches(/^(?=.*[a-zA-Z])(?=.*\d)[a-zA-Z\d!@#$%^&*()_+\-=\[\]{};:'",.<>/?]{6,32}$/, @Matches(
{ message: '密码必须包含字母和数字且长度在6~32之间' } /^(?=.*[a-zA-Z])(?=.*\d)[a-zA-Z\d!@#$%^&*()_+\-=\[\]{};:'",.<>/?]{6,32}$/,
) { message: '密码必须包含字母和数字且长度在6~32之间' },
password: string; )
} password: string;
}

View File

@@ -1,20 +1,27 @@
import { Column, CreateDateColumn, DeleteDateColumn, Entity, Index, PrimaryGeneratedColumn } from "typeorm"; import {
Column,
CreateDateColumn,
DeleteDateColumn,
Entity,
Index,
PrimaryGeneratedColumn,
} from 'typeorm';
@Entity() @Entity()
@Index(['sessionId', 'userId']) @Index(['sessionId', 'userId'])
export class UserSession { export class UserSession {
@PrimaryGeneratedColumn('uuid') @PrimaryGeneratedColumn('uuid')
id: string; id: string;
@Column({ length: 36 }) @Column({ length: 36 })
sessionId: string; sessionId: string;
@Column({ length: 36 }) @Column({ length: 36 })
userId: string; userId: string;
@CreateDateColumn({ precision: 3 }) @CreateDateColumn({ precision: 3 })
createdAt: Date; createdAt: Date;
@DeleteDateColumn({ nullable: true, precision: 3 }) @DeleteDateColumn({ nullable: true, precision: 3 })
deletedAt: Date; deletedAt: Date;
} }

View File

@@ -1,72 +1,87 @@
import { BeforeInsert, Column, CreateDateColumn, DeleteDateColumn, Entity, Index, PrimaryGeneratedColumn, UpdateDateColumn } from "typeorm"; import {
BeforeInsert,
Column,
CreateDateColumn,
DeleteDateColumn,
Entity,
Index,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
@Entity() @Entity()
@Index("IDX_user_userid", ["userId"], { unique: true }) @Index('IDX_user_userid', ['userId'], { unique: true })
@Index("IDX_user_username", ["username"], { unique: true }) @Index('IDX_user_username', ['username'], { unique: true })
@Index("IDX_user_email", ["email"], { unique: true, where: "email IS NOT NULL" }) @Index('IDX_user_email', ['email'], {
@Index("IDX_user_phone", ["phone"], { unique: true, where: "phone IS NOT NULL" }) unique: true,
where: 'email IS NOT NULL',
})
@Index('IDX_user_phone', ['phone'], {
unique: true,
where: 'phone IS NOT NULL',
})
export class User { export class User {
@PrimaryGeneratedColumn('uuid') @PrimaryGeneratedColumn('uuid')
userId: string; userId: string;
@Column({ length: 32 }) @Column({ length: 32 })
username: string; username: string;
@Column({ length: 30 }) @Column({ length: 30 })
nickname: string; nickname: string;
@BeforeInsert() @BeforeInsert()
generateDefaults() { generateDefaults() {
if (!this.username) { if (!this.username) {
this.username = `user_${uuidv4().replace(/-/g, '').slice(0, 27)}`; this.username = `user_${uuidv4().replace(/-/g, '').slice(0, 27)}`;
}
if (!this.nickname) {
this.nickname = `用户_${uuidv4().replace(/-/g, '').slice(0, 8)}`;
}
} }
if (!this.nickname) {
this.nickname = `用户_${uuidv4().replace(/-/g, '').slice(0, 8)}`;
}
}
@Column({ nullable: true, type: 'char', length: 32 }) @Column({ nullable: true, type: 'char', length: 32 })
salt: string; salt: string;
@Column({ nullable: true, type: 'char', length: 64 }) @Column({ nullable: true, type: 'char', length: 64 })
password_hash: string; password_hash: string;
@Column({ @Column({
nullable: true, nullable: true,
length: 254, length: 254,
transformer: { transformer: {
to: (value: string | null) => value?.trim() || null, to: (value: string | null) => value?.trim() || null,
from: (value: string | null) => value, from: (value: string | null) => value,
} },
})// RFC 5321 }) // RFC 5321
email: string | null; email: string | null;
@Column({ @Column({
nullable: true, nullable: true,
length: 20, length: 20,
transformer: { transformer: {
to: (value: string | null) => value?.trim() || null, to: (value: string | null) => value?.trim() || null,
from: (value: string | null) => value, from: (value: string | null) => value,
} },
})// China Mainland }) // China Mainland
phone: string | null; phone: string | null;
@Column({ @Column({
nullable: true, nullable: true,
transformer: { transformer: {
to: (value: string | null) => value?.trim() || null, to: (value: string | null) => value?.trim() || null,
from: (value: string | null) => value, from: (value: string | null) => value,
} },
}) })
avatar: string; avatar: string;
@CreateDateColumn({ precision: 3 }) @CreateDateColumn({ precision: 3 })
createdAt: Date; createdAt: Date;
@UpdateDateColumn({ precision: 3 }) @UpdateDateColumn({ precision: 3 })
updatedAt: Date; updatedAt: Date;
@DeleteDateColumn({ nullable: true, precision: 3 }) @DeleteDateColumn({ nullable: true, precision: 3 })
deletedAt: Date; deletedAt: Date;
} }

View File

@@ -1,47 +1,46 @@
import { InjectRepository } from "@nestjs/typeorm"; import { InjectRepository } from '@nestjs/typeorm';
import { Injectable } from "@nestjs/common"; import { Injectable } from '@nestjs/common';
import { UserSession } from "../entities/user-session.entity"; import { UserSession } from '../entities/user-session.entity';
import { Repository } from "typeorm"; import { Repository } from 'typeorm';
@Injectable() @Injectable()
export class UserSessionService { export class UserSessionService {
constructor( constructor(
@InjectRepository(UserSession) @InjectRepository(UserSession)
private readonly userSessionRepository: Repository<UserSession>, private readonly userSessionRepository: Repository<UserSession>,
) { } ) {}
async createSession(userId: string, sessionId: string): Promise<UserSession> {
const session = this.userSessionRepository.create({
userId,
sessionId,
});
return await this.userSessionRepository.save(session);
}
async createSession(userId: string, sessionId: string): Promise<UserSession> { async isSessionValid(userId: string, sessionId: string): Promise<boolean> {
const session = this.userSessionRepository.create({ const session = await this.userSessionRepository.findOne({
userId, where: {
sessionId, userId,
}); sessionId,
return await this.userSessionRepository.save(session); deletedAt: null,
},
});
return !!session;
}
async invalidateSession(userId: string, sessionId: string): Promise<void> {
const session = await this.userSessionRepository.findOne({
where: {
userId,
sessionId,
deletedAt: null,
},
});
if (session) {
await this.userSessionRepository.softDelete(session.id);
} }
}
async isSessionValid(userId: string, sessionId: string): Promise<boolean> { }
const session = await this.userSessionRepository.findOne({
where: {
userId,
sessionId,
deletedAt: null,
}
});
return !!session;
}
async invalidateSession(userId: string, sessionId: string): Promise<void> {
const session = await this.userSessionRepository.findOne({
where: {
userId,
sessionId,
deletedAt: null,
}
});
if (session) {
await this.userSessionRepository.softDelete(session.id);
}
}
}

View File

@@ -6,25 +6,21 @@ import { AuthService } from 'src/auth/auth.service';
@Controller('user') @Controller('user')
export class UserController { export class UserController {
constructor(
private readonly userService: UserService,
private readonly authService: AuthService,
) {}
constructor( @UseGuards(AuthGuard('jwt'))
private readonly userService: UserService, @Get('me')
private readonly authService: AuthService, async getMe(@Request() req) {
) { } const { user } = req;
return this.userService.findOne({ userId: user.userId });
}
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'))
@Get('me') @Put('password')
async getMe(@Request() req) { async update(@Request() req, @Body() dto: UpdateUserPasswordDto) {
const { user } = req; return this.userService.setPassword(req.user.userId, dto.password);
return this.userService.findOne({ userId: user.userId }); }
}
@UseGuards(AuthGuard('jwt'))
@Put('password')
async update(
@Request() req,
@Body() dto: UpdateUserPasswordDto,
) {
return this.userService.setPassword(req.user.userId, dto.password);
}
} }

View File

@@ -8,12 +8,12 @@ import { AuthModule } from 'src/auth/auth.module';
import { UserSessionService } from './services/user-session.service'; import { UserSessionService } from './services/user-session.service';
@Module({ @Module({
imports: [ imports: [
TypeOrmModule.forFeature([User, UserSession]), TypeOrmModule.forFeature([User, UserSession]),
forwardRef(() => AuthModule),// 解决循环依赖问题 forwardRef(() => AuthModule), // 解决循环依赖问题
], ],
controllers: [UserController], controllers: [UserController],
providers: [UserService, UserSessionService], providers: [UserService, UserSessionService],
exports: [UserService, UserSessionService], exports: [UserService, UserSessionService],
}) })
export class UserModule { } export class UserModule {}

View File

@@ -1,124 +1,138 @@
import { BadRequestException, ConflictException, Injectable } from '@nestjs/common'; import {
BadRequestException,
ConflictException,
Injectable,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { User } from './entities/user.entity'; import { User } from './entities/user.entity';
import { QueryFailedError, Repository } from 'typeorm'; import { QueryFailedError, Repository } from 'typeorm';
import { createHash, ECDH } from 'crypto'; import { createHash } from 'crypto';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
type UserFindOptions = Partial<Pick<User, 'userId' | 'username' | 'phone' | 'email'>>; type UserFindOptions = Partial<
Pick<User, 'userId' | 'username' | 'phone' | 'email'>
>;
@Injectable() @Injectable()
export class UserService { export class UserService {
constructor( constructor(
@InjectRepository(User) @InjectRepository(User)
private readonly userRepository: Repository<User>, private readonly userRepository: Repository<User>,
) { } ) {}
async findOne(options: UserFindOptions | UserFindOptions[], additionalOptions?: { withDeleted?: boolean }): Promise<User | null> { async findOne(
if (Object.keys(options).length === 0) { options: UserFindOptions | UserFindOptions[],
throw new BadRequestException('查询条件不能为空'); additionalOptions?: { withDeleted?: boolean },
} ): Promise<User | null> {
return this.userRepository.findOne({ if (Object.keys(options).length === 0) {
where: options, throw new BadRequestException('查询条件不能为空');
withDeleted: additionalOptions?.withDeleted || false, }
}); return this.userRepository.findOne({
where: options,
withDeleted: additionalOptions?.withDeleted || false,
});
}
async create(user: Partial<User>): Promise<User> {
try {
const newUser = this.userRepository.create(user);
return await this.userRepository.save(newUser);
} catch (error) {
if (error instanceof QueryFailedError) {
throw new ConflictException(this.getDuplicateErrorMessage(error));
}
throw new BadRequestException('创建用户失败');
}
}
async update(userId: string, user: Partial<User>): Promise<User> {
const existingUser = await this.userRepository.findOne({
where: { userId },
});
if (!existingUser) {
throw new BadRequestException('User not found');
}
try {
Object.assign(existingUser, user);
return await this.userRepository.save(existingUser);
} catch (error) {
if (error instanceof QueryFailedError) {
throw new ConflictException(this.getDuplicateErrorMessage(error));
}
}
}
async delete(userId: string, soft: boolean) {
const existingUser = await this.userRepository.findOne({
where: { userId },
withDeleted: true,
});
if (!existingUser) {
throw new BadRequestException('用户不存在');
} }
async create(user: Partial<User>): Promise<User> { if (existingUser.deletedAt && soft) {
try { throw new BadRequestException('账户已注销,不得重复操作');
const newUser = this.userRepository.create(user);
return await this.userRepository.save(newUser);
} catch (error) {
if (error instanceof QueryFailedError) {
throw new ConflictException(this.getDuplicateErrorMessage(error));
}
throw new BadRequestException('创建用户失败');
}
} }
async update(userId: string, user: Partial<User>): Promise<User> { if (!existingUser.deletedAt && !soft) {
const existingUser = await this.userRepository.findOne({ where: { userId } }); throw new BadRequestException('账号未注销,请先注销再执行删除操作');
if (!existingUser) {
throw new BadRequestException('User not found');
}
try {
Object.assign(existingUser, user);
return await this.userRepository.save(existingUser);
} catch (error) {
if (error instanceof QueryFailedError) {
throw new ConflictException(this.getDuplicateErrorMessage(error));
}
}
} }
async delete(userId: string, soft: boolean) { return soft
const existingUser = await this.userRepository.findOne({ where: { userId }, withDeleted: true }); ? await this.userRepository.softDelete(existingUser.userId)
if (!existingUser) { : await this.userRepository.delete(existingUser.userId);
throw new BadRequestException('用户不存在'); }
}
if (existingUser.deletedAt && soft) { hashPassword(password: string, salt: string): string {
throw new BadRequestException('账户已注销,不得重复操作') return createHash('sha256').update(`${password}${salt}`).digest('hex');
} }
if (!existingUser.deletedAt && !soft) { generateSalt(): string {
throw new BadRequestException('账号未注销,请先注销再执行删除操作') return uuid().replace(/-/g, '');
} }
return soft async setPassword(userId: string, password: string): Promise<User> {
? await this.userRepository.softDelete(existingUser.userId) const user = await this.userRepository.findOne({ where: { userId } });
: await this.userRepository.delete(existingUser.userId) if (!user) {
throw new BadRequestException('User not found');
} }
const salt = this.generateSalt();
user.password_hash = this.hashPassword(password, salt);
user.salt = salt;
return this.userRepository.save(user);
}
hashPassword(password: string, salt: string): string { private getDuplicateErrorMessage(error: QueryFailedError): string {
return createHash('sha256').update(`${password}${salt}`).digest('hex'); // 根据具体的错误信息返回友好的提示
if (error.message.includes('IDX_user_username')) {
return '账户名已被使用';
} }
if (error.message.includes('IDX_user_email')) {
generateSalt(): string { return '邮箱已被使用';
return uuid().replace(/-/g, '');
} }
if (error.message.includes('IDX_user_phone')) {
async setPassword(userId: string, password: string): Promise<User> { return '手机号已被使用';
const user = await this.userRepository.findOne({ where: { userId } });
if (!user) {
throw new BadRequestException('User not found');
}
const salt = this.generateSalt();
user.password_hash = this.hashPassword(password, salt);
user.salt = salt;
return this.userRepository.save(user);
} }
return '数据已存在,请检查输入';
}
private getDuplicateErrorMessage(error: QueryFailedError): string { async list(page = 1, pageSize = 20) {
// 根据具体的错误信息返回友好的提示 const queryBuilder = this.userRepository.createQueryBuilder('user');
if (error.message.includes('IDX_user_username')) {
return '账户名已被使用';
}
if (error.message.includes('IDX_user_email')) {
return '邮箱已被使用';
}
if (error.message.includes('IDX_user_phone')) {
return '手机号已被使用';
}
return '数据已存在,请检查输入';
}
async list(page = 1, pageSize = 20) { queryBuilder.withDeleted();
const queryBuilder = this.userRepository.createQueryBuilder('user')
queryBuilder.withDeleted(); queryBuilder.orderBy('user.createdAt', 'DESC');
queryBuilder.orderBy('user.createdAt', 'DESC'); queryBuilder.skip((page - 1) * pageSize);
queryBuilder.take(pageSize);
queryBuilder.skip((page - 1) * pageSize); const [items, total] = await queryBuilder.getManyAndCount();
queryBuilder.take(pageSize); return {
items,
const [items, total] = await queryBuilder.getManyAndCount(); total,
return { page,
items, pageSize,
total, };
page, }
pageSize,
}
}
} }

View File

@@ -1,19 +1,19 @@
import { IsEnum, IsString, Length, ValidateIf } from "class-validator"; import { IsEnum, IsString, Length, ValidateIf } from 'class-validator';
export class SendVerificationCodeDto { export class SendVerificationCodeDto {
@IsEnum(['phone', 'email'], { message: '请求类型错误' }) @IsEnum(['phone', 'email'], { message: '请求类型错误' })
targetType: 'phone' | 'email'; targetType: 'phone' | 'email';
@IsEnum(['login'], { message: '请求类型错误' }) @IsEnum(['login'], { message: '请求类型错误' })
type: 'login' type: 'login';
@ValidateIf(o => o.targetType === 'phone') @ValidateIf((o) => o.targetType === 'phone')
@IsString({ message: '手机号必须输入' }) @IsString({ message: '手机号必须输入' })
@Length(11, 11, { message: '手机号异常' })// 中国大陆11位数字 @Length(11, 11, { message: '手机号异常' }) // 中国大陆11位数字
phone?: string; phone?: string;
@ValidateIf(o => o.targetType === 'email') @ValidateIf((o) => o.targetType === 'email')
@IsString({ message: '邮箱必须输入' }) @IsString({ message: '邮箱必须输入' })
@Length(6, 254, { message: '邮箱异常' })// RFC 5321 @Length(6, 254, { message: '邮箱异常' }) // RFC 5321
email?: string; email?: string;
} }

View File

@@ -4,25 +4,22 @@ import { VerificationService } from './verification.service';
@Controller('verification') @Controller('verification')
export class VerificationController { export class VerificationController {
constructor(private readonly verificationService: VerificationService) {}
constructor( @Post('send')
private readonly verificationService: VerificationService, async sendVerificationCode(@Body() dto: SendVerificationCodeDto) {
) { } switch (dto.type) {
case 'login':
@Post('send') switch (dto.targetType) {
async sendVerificationCode(@Body() dto: SendVerificationCodeDto) { case 'phone':
switch (dto.type) { return this.verificationService.sendPhoneCode(dto.phone, dto.type);
case 'login': case 'email':
switch (dto.targetType) { return this.verificationService.sendEmailCode(dto.email, dto.type);
case 'phone': default:
return this.verificationService.sendPhoneCode(dto.phone, dto.type); throw new BadRequestException('不支持的目标类型');
case 'email':
return this.verificationService.sendEmailCode(dto.email, dto.type);
default:
throw new BadRequestException('不支持的目标类型');
}
default:
throw new BadRequestException('不支持的验证码类型');
} }
default:
throw new BadRequestException('不支持的验证码类型');
} }
}
} }

View File

@@ -9,4 +9,4 @@ import { NotificationModule } from 'src/notification/notification.module';
exports: [VerificationService], exports: [VerificationService],
imports: [NotificationModule], imports: [NotificationModule],
}) })
export class VerificationModule { } export class VerificationModule {}

View File

@@ -3,97 +3,96 @@ import { NotificationService } from 'src/notification/notification.service';
@Injectable() @Injectable()
export class VerificationService { export class VerificationService {
private readonly logger = new Logger(VerificationService.name);
private readonly logger = new Logger(VerificationService.name); constructor(private readonly notificationService: NotificationService) {}
constructor( private pool: Map<
private readonly notificationService: NotificationService, string,
) { } {
code: string;
private pool: Map<string, { createdAt: number;
code: string; expiredAt: number;
createdAt: number; tryCount: number;
expiredAt: number; maxTryCount: number;
tryCount: number;
maxTryCount: number;
}> = new Map();
async sendPhoneCode(phone: string, type: 'login') {
const key = `phone:${phone}:${type}`;
// 检测是否在冷却时间内
// TODO
// 生成验证码
const code = this.generateCode();
this.logger.log(`Phone[${phone}] code: ${code}`);
// 发送验证码
// await this.notificationService.sendSMS(phone, type, code);
// 存储验证码
this.saveCode(key, code);
return true;
} }
> = new Map();
async sendEmailCode(email: string, type: 'login') { async sendPhoneCode(phone: string, type: 'login') {
const key = `email:${email}:${type}`; const key = `phone:${phone}:${type}`;
// 检测是否在冷却时间内 // 检测是否在冷却时间内
// TODO // TODO
// 生成验证码 // 生成验证码
const code = this.generateCode(); const code = this.generateCode();
this.logger.log(`Email[${email}] code: ${code}`); this.logger.log(`Phone[${phone}] code: ${code}`);
// 发送验证码
// TODO
// 存储验证码 // 发送验证码
this.saveCode(key, code); // await this.notificationService.sendSMS(phone, type, code);
return true; // 存储验证码
this.saveCode(key, code);
return true;
}
async sendEmailCode(email: string, type: 'login') {
const key = `email:${email}:${type}`;
// 检测是否在冷却时间内
// TODO
// 生成验证码
const code = this.generateCode();
this.logger.log(`Email[${email}] code: ${code}`);
// 发送验证码
// TODO
// 存储验证码
this.saveCode(key, code);
return true;
}
private saveCode(key: string, code: string) {
this.pool.set(key, {
code: code,
createdAt: Date.now(),
expiredAt: Date.now() + 10 * 60 * 1000, // 10分钟过期
tryCount: 0,
maxTryCount: 5,
});
}
verifyPhoneCode(phone: string, code: string, type: 'login') {
const key = `phone:${phone}:${type}`;
return this.verifyCode(key, code);
}
verifyEmailCode(email: string, code: string, type: 'login') {
const key = `email:${email}:${type}`;
return this.verifyCode(key, code);
}
/**
* @returns 0: 验证码正确, -1: 验证码不存在或已过期, -2: 验证码错误, -3: 超过最大尝试次数
*/
private verifyCode(key: string, code: string) {
const data = this.pool.get(key);
if (!data) {
return -1;
} }
if (data.tryCount >= data.maxTryCount) {
private saveCode(key: string, code: string) { return -3;
this.pool.set(key, {
code: code,
createdAt: Date.now(),
expiredAt: Date.now() + 10 * 60 * 1000, // 10分钟过期
tryCount: 0,
maxTryCount: 5,
});
} }
if (data.expiredAt < Date.now()) {
verifyPhoneCode(phone: string, code: string, type: 'login') { return -1;
const key = `phone:${phone}:${type}`;
return this.verifyCode(key, code);
} }
if (data.code !== code) {
verifyEmailCode(email: string, code: string, type: 'login') { data.tryCount++;
const key = `email:${email}:${type}`; return -2;
return this.verifyCode(key, code);
} }
this.pool.delete(key);
return 0;
}
/** private generateCode() {
* @returns 0: 验证码正确, -1: 验证码不存在或已过期, -2: 验证码错误, -3: 超过最大尝试次数 return Math.floor(100000 + Math.random() * 900000).toString();
*/ }
private verifyCode(key: string, code: string) {
const data = this.pool.get(key);
if (!data) {
return -1;
}
if (data.tryCount >= data.maxTryCount) {
return -3;
}
if (data.expiredAt < Date.now()) {
return -1;
}
if (data.code !== code) {
data.tryCount++;
return -2;
}
this.pool.delete(key);
return 0;
}
private generateCode() {
return Math.floor(100000 + Math.random() * 900000).toString();
}
} }