format + lint

This commit is contained in:
2025-06-14 14:12:18 +08:00
parent 95e8f8c648
commit 1de3a3f197
69 changed files with 1756 additions and 1583 deletions

View File

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

View File

@@ -1,13 +1,18 @@
import { Body, 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";
import {
Body,
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')
export class AdminPermissionController {
constructor(
private readonly permissionService: PermissionService,
) { }
constructor(private readonly permissionService: PermissionService) {}
@Get()
async list() {
@@ -15,17 +20,12 @@ export class AdminPermissionController {
}
@Post()
async create(
@Body() dto: CreatePermissionDto
) {
async create(@Body() dto: CreatePermissionDto) {
return this.permissionService.create(dto);
}
@Delete(':id')
async delete(
@Param('id', new ParseUUIDPipe({ version: '4' })) id: string,
) {
async delete(@Param('id', new ParseUUIDPipe({ version: '4' })) id: string) {
return this.permissionService.delete(id);
}
}

View File

@@ -1,21 +1,29 @@
import { Body, Controller, 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";
import {
Body,
Controller,
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')
export class AdminRolePermissionController {
constructor(
private readonly rolePermissionService: RolePermissionService,
private readonly permissionService: PermissionService,
) { }
) {}
@Get()
async getRolePermissions(
@Param('roleId', new ParseUUIDPipe({ version: '4' })) roleId: string,
) {
const permissionIds = await this.rolePermissionService.findPermissionIdsByRoleIds([roleId]);
const permissionIds =
await this.rolePermissionService.findPermissionIdsByRoleIds([roleId]);
return await this.permissionService.findPermissionByIds(permissionIds);
}
@@ -24,7 +32,10 @@ export class AdminRolePermissionController {
@Param('roleId', new ParseUUIDPipe({ version: '4' })) roleId: string,
@Body() dto: SetRolePermissionsDto,
) {
return await this.rolePermissionService.addRolePermissions(roleId, dto.permissionIds);
return await this.rolePermissionService.addRolePermissions(
roleId,
dto.permissionIds,
);
}
@Delete()
@@ -32,6 +43,9 @@ export class AdminRolePermissionController {
@Param('roleId', new ParseUUIDPipe({ version: '4' })) roleId: string,
@Body() dto: SetRolePermissionsDto,
) {
return await this.rolePermissionService.deleteRolePermissions(roleId, dto.permissionIds);
return await this.rolePermissionService.deleteRolePermissions(
roleId,
dto.permissionIds,
);
}
}

View File

@@ -1,13 +1,18 @@
import { Body, 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";
import {
Body,
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')
export class AdminRoleController {
constructor(
private readonly roleService: RoleService,
) { }
constructor(private readonly roleService: RoleService) {}
@Get()
async list() {
@@ -15,16 +20,12 @@ export class AdminRoleController {
}
@Post()
async create(
@Body() dto: CreateRoleDto
) {
async create(@Body() dto: CreateRoleDto) {
return this.roleService.create(dto);
}
@Delete(':id')
async delete(
@Param('id', new ParseUUIDPipe({ version: '4' })) id: string,
) {
async delete(@Param('id', new ParseUUIDPipe({ version: '4' })) id: string) {
return this.roleService.delete(id);
}
}

View File

@@ -1,16 +1,23 @@
import { Body, Controller, Delete, 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";
import {
Body,
Controller,
Delete,
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')
export class AdminUserRoleController {
constructor(
private readonly userRoleService: UserRoleService,
private readonly roleService: RoleService,
) { }
) {}
@Get()
async getUserRoles(

View File

@@ -1,22 +1,27 @@
import { Body, Controller, Delete, Get, Param, 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";
import {
Body,
Controller,
Delete,
Get,
Param,
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')
export class AdminUserController {
constructor(
private readonly userService: UserService,
) { }
constructor(private readonly userService: UserService) {}
@Get()
async list(
@Query() listDto: ListDto
) {
async list(@Query() listDto: ListDto) {
return this.userService.list(listDto.page, listDto.pageSize);
}
@@ -28,18 +33,20 @@ export class AdminUserController {
}
@Post()
async create(
@Body() createDto: CreateDto
) {
async create(@Body() createDto: CreateDto) {
return this.userService.create({
...createDto,
...createDto.password && (() => {
...(createDto.password &&
(() => {
const salt = this.userService.generateSalt();
return {
salt,
password_hash: this.userService.hashPassword(createDto.password, salt),
}
})(),
password_hash: this.userService.hashPassword(
createDto.password,
salt,
),
};
})()),
});
}

View File

@@ -1,13 +1,19 @@
import { Body, 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";
import {
Body,
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')
export class AdminWebBlogController {
constructor(
private readonly adminWebBlogService: BlogService,
) { }
constructor(private readonly adminWebBlogService: BlogService) {}
@Get()
async list() {
@@ -15,9 +21,7 @@ export class AdminWebBlogController {
}
@Post()
async create(
@Body() dto: CreateBlogDto,
) {
async create(@Body() dto: CreateBlogDto) {
return this.adminWebBlogService.create(dto);
}
@@ -30,16 +34,12 @@ export class AdminWebBlogController {
}
@Get(':id')
async get(
@Param('id', new ParseUUIDPipe({ version: '4' })) id: string,
) {
async get(@Param('id', new ParseUUIDPipe({ version: '4' })) id: string) {
return this.adminWebBlogService.findById(id);
}
@Delete(':id')
async remove(
@Param('id', new ParseUUIDPipe({ version: '4' })) id: string,
) {
async remove(@Param('id', new ParseUUIDPipe({ version: '4' })) id: string) {
return this.adminWebBlogService.remove(id);
}
}

View File

@@ -1,13 +1,19 @@
import { Body, 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";
import {
Body,
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')
export class AdminWebResourceController {
constructor(
private readonly resourceService: ResourceService,
) { }
constructor(private readonly resourceService: ResourceService) {}
@Get()
async list() {
@@ -27,15 +33,13 @@ export class AdminWebResourceController {
@Put(':id')
async update(
@Param('id', new ParseUUIDPipe({ version: '4' })) id: string,
@Body() data: CreateResourceDto
@Body() data: CreateResourceDto,
) {
return this.resourceService.update(id, data);
}
@Delete(':id')
async delete(
@Param('id', new ParseUUIDPipe({ version: '4' })) id: string,
) {
async delete(@Param('id', new ParseUUIDPipe({ version: '4' })) id: string) {
return this.resourceService.delete(id);
}
}

View File

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

View File

@@ -1,4 +1,4 @@
import { ArrayMinSize, IsArray, IsUUID } from "class-validator";
import { ArrayMinSize, IsArray, IsUUID } from 'class-validator';
export class DeleteRolePermissionsDto {
@IsArray()

View File

@@ -1,4 +1,4 @@
import { ArrayMinSize, IsArray, IsUUID } from "class-validator";
import { ArrayMinSize, IsArray, IsUUID } from 'class-validator';
export class SetRolePermissionsDto {
@IsArray()

View File

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

View File

@@ -1,4 +1,4 @@
import { IsBoolean, IsDateString, IsOptional, IsUUID } from "class-validator";
import { IsBoolean, IsDateString, IsOptional, IsUUID } from 'class-validator';
export class CreateUserRoleDto {
@IsUUID('4')

View File

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

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

View File

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

View File

@@ -1,4 +1,10 @@
import { IsEmail, IsOptional, IsString, Length, Matches } from "class-validator";
import {
IsEmail,
IsOptional,
IsString,
Length,
Matches,
} from 'class-validator';
export class UpdateDto {
@IsString({ message: '用户名不得为空' })
@@ -14,7 +20,7 @@ export class UpdateDto {
@Length(6, 254, {
message: '邮箱长度只能为6~254',
// 仅在值不为 null 或 undefined 时验证
always: false
always: false,
})
email?: string;
@@ -23,7 +29,7 @@ export class UpdateDto {
@Matches(/^1[3456789]\d{9}$/, {
message: '请输入有效的手机号码',
// 仅在值不为 null 或 undefined 时验证
always: false
always: false,
})
phone?: string;
}

View File

@@ -1,4 +1,4 @@
import { IsString } from "class-validator";
import { IsString } from 'class-validator';
export class CreateBlogDto {
@IsString()

View File

@@ -1,5 +1,5 @@
import { Type } from "class-transformer";
import { IsString, ValidateNested } from "class-validator";
import { Type } from 'class-transformer';
import { IsString, ValidateNested } from 'class-validator';
class ResourceTagDto {
@IsString()

View File

@@ -1,5 +1,5 @@
import { Type } from 'class-transformer';
import { IsInt, IsOptional, Max, Min } from 'class-validator';
import { IsInt, IsOptional, Min } from 'class-validator';
export class PaginationDto {
@IsOptional()

View File

@@ -42,4 +42,4 @@ import { OssModule } from './oss/oss.module';
controllers: [AppController],
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 { AuthService } from './auth.service';
import { AuthGuard } from '@nestjs/passport';
@@ -6,11 +13,10 @@ import { UserSessionService } from 'src/user/services/user-session.service';
@Controller('auth')
export class AuthController {
constructor(
private readonly authService: AuthService,
private readonly userSessionService: UserSessionService,
) { }
) {}
@Post('login')
async login(@Body() loginDto: LoginDto) {

View File

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

View File

@@ -10,24 +10,22 @@ import { VerificationService } from 'src/verification/verification.service';
@Injectable()
export class AuthService {
constructor(
private readonly userService: UserService,
private readonly jwtService: JwtService,
private readonly userSessionService: UserSessionService,
private readonly verificationService: VerificationService,
) { }
) {}
async loginWithPassword(loginDto: LoginDto) {
const { account, password } = loginDto;
// 依次使用邮箱登录、手机号、账号
const user = await this.userService.findOne([
{ email: account },
{ phone: account },
{ username: account },
], {
const user = await this.userService.findOne(
[{ email: account }, { phone: account }, { username: account }],
{
withDeleted: true,
});
},
);
if (user && user.deletedAt !== null) {
throw new BadRequestException('该账号注销中');
@@ -46,13 +44,17 @@ export class AuthService {
// 登录成功颁发token
return {
token: await this.generateToken(user),
}
};
}
async loginWithPhone(loginDto: LoginDto) {
const { phone, code } = loginDto;
// 先判断验证码是否正确
const isValid = this.verificationService.verifyPhoneCode(phone, code, 'login');
const isValid = this.verificationService.verifyPhoneCode(
phone,
code,
'login',
);
switch (isValid) {
case 0:
break;
@@ -77,20 +79,25 @@ export class AuthService {
user = await this.userService.create({ phone: phone });
}
if (!user || !user.userId) {// 注册失败或用户信息错误
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');
const isValid = this.verificationService.verifyEmailCode(
email,
code,
'login',
);
switch (isValid) {
case 0:
break;
@@ -115,14 +122,15 @@ export class AuthService {
user = await this.userService.create({ email: email });
}
if (!user || !user.userId) {// 注册失败或用户信息错误
if (!user || !user.userId) {
// 注册失败或用户信息错误
throw new BadRequestException('请求失败,请稍后再试');
}
// 登录颁发token
return {
token: await this.generateToken(user),
}
};
}
private hashPassword(password: string, salt: string): string {
@@ -133,13 +141,15 @@ export class AuthService {
const payload = {
userId: user.userId,
sessionId: uuidv4(),
}
};
// 存储
await this.userSessionService.createSession(payload.userId, payload.sessionId);
await this.userSessionService.createSession(
payload.userId,
payload.sessionId,
);
// 颁发token
return this.jwtService.sign(payload);
}
}

View File

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

View File

@@ -1,6 +1,5 @@
import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common";
import { AuthGuard } from "@nestjs/passport";
import { Observable, retry } from "rxjs";
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class OptionalAuthGuard extends AuthGuard('jwt') implements CanActivate {
@@ -9,11 +8,18 @@ export class OptionalAuthGuard extends AuthGuard('jwt') implements CanActivate {
await super.canActivate(context);
return true;
} 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>(
err: any,
user: any,
// info: any,
// context: ExecutionContext,
// status?: any,
): TUser {
if (err || !user) {
return null; // 如果没有用户信息返回null
}

View File

@@ -1,8 +1,8 @@
import { Injectable, UnauthorizedException } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { PassportStrategy } from "@nestjs/passport";
import { ExtractJwt, Strategy } from "passport-jwt";
import { UserSessionService } from "src/user/services/user-session.service";
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { UserSessionService } from 'src/user/services/user-session.service';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
@@ -14,13 +14,16 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.get<string>('JWT_SECRET', 'tone-page'),
})
});
}
async validate(payload: any) {
const { userId, sessionId } = payload ?? {};
const isValidSession = await this.userSessionService.isSessionValid(userId, sessionId);
const isValidSession = await this.userSessionService.isSessionValid(
userId,
sessionId,
);
if (!isValidSession) {
throw new UnauthorizedException('登录凭证已过期,请重新登录');
}
@@ -28,6 +31,6 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
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 { OptionalAuthGuard } from 'src/auth/strategies/OptionalAuthGuard';
import { UserService } from 'src/user/user.service';
@@ -6,11 +16,10 @@ import { createBlogCommentDto } from './dto/create.blogcomment.dto';
@Controller('blog')
export class BlogController {
constructor(
private readonly blogService: BlogService,
private readonly userService: UserService,
) { }
) {}
@Get()
getBlogs() {
@@ -18,9 +27,7 @@ export class BlogController {
}
@Get(':id')
async getBlog(
@Param('id', new ParseUUIDPipe({ version: '4' })) id: string,
) {
async getBlog(@Param('id', new ParseUUIDPipe({ version: '4' })) id: string) {
const blog = await this.blogService.findById(id);
if (!blog) throw new BadRequestException('文章不存在');
@@ -58,13 +65,21 @@ export class BlogController {
const blog = await this.blogService.findById(id);
if (!blog) throw new BadRequestException('文章不存在');
let user = userId ? await this.userService.findOne({ userId }) : null;
const user = userId ? await this.userService.findOne({ userId }) : null;
// 获取IP归属地
const ip = req.ip || req.socket.remoteAddress || req.headers['x-forwarded-for'] || '未知';
const ip =
req.ip ||
req.socket.remoteAddress ||
req.headers['x-forwarded-for'] ||
'未知';
let address = '未知';
if (!['::1'].includes(ip)) {
const addressRes = await (await fetch(`https://mesh.if.iqiyi.com/aid/ip/info?version=1.1.1&ip=${ip}`)).json();
const addressRes = await (
await fetch(
`https://mesh.if.iqiyi.com/aid/ip/info?version=1.1.1&ip=${ip}`,
)
).json();
if (addressRes?.code == 0) {
const country: string = addressRes?.data?.countryCN || '未知';
const province: string = addressRes?.data?.provinceCN || '中国';

View File

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

View File

@@ -3,25 +3,23 @@ import { InjectRepository } from '@nestjs/typeorm';
import { Blog } from './entity/Blog.entity';
import { Repository } from 'typeorm';
import { BlogComment } from './entity/BlogComment';
import { UserService } from 'src/user/user.service';
@Injectable()
export class BlogService {
constructor(
@InjectRepository(Blog)
private readonly blogRepository: Repository<Blog>,
@InjectRepository(BlogComment)
private readonly blogCommentRepository: Repository<BlogComment>,
) { }
) {}
async list() {
return this.blogRepository.find({
where: { deletedAt: null },
order: {
createdAt: 'DESC',
}
})
},
});
}
async create(blog: Partial<Blog>) {
@@ -54,7 +52,7 @@ export class BlogService {
relations: ['user'],
order: {
createdAt: 'DESC',
}
},
});
}

View File

@@ -1,4 +1,4 @@
import { IsOptional, IsString, IsUUID } from "class-validator";
import { IsOptional, IsString, IsUUID } from 'class-validator';
export class createBlogCommentDto {
@IsString({ message: '评论内容不能为空' })

View File

@@ -1,5 +1,13 @@
import { Column, CreateDateColumn, DeleteDateColumn, Entity, OneToMany, PrimaryGeneratedColumn, UpdateDateColumn } from "typeorm";
import { BlogComment } from "./BlogComment";
import {
Column,
CreateDateColumn,
DeleteDateColumn,
Entity,
OneToMany,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import { BlogComment } from './BlogComment';
@Entity()
export class Blog {
@@ -30,6 +38,6 @@ export class Blog {
// 权限关系 TODO
// 关系
@OneToMany(() => BlogComment, blog => blog.id)
@OneToMany(() => BlogComment, (blog) => blog.id)
comments: BlogComment[];
}

View File

@@ -1,5 +1,13 @@
import { User } from "src/user/entities/user.entity";
import { Column, CreateDateColumn, DeleteDateColumn, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
import { User } from 'src/user/entities/user.entity';
import {
Column,
CreateDateColumn,
DeleteDateColumn,
Entity,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
} from 'typeorm';
@Entity()
export class BlogComment {

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);

View File

@@ -1,8 +1,8 @@
import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common";
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { PermissionService } from "src/role/services/permission.service";
import { RolePermissionService } from "src/role/services/role-permission.service";
import { UserRoleService } from "src/role/services/user-role.service";
import { PermissionService } from 'src/role/services/permission.service';
import { RolePermissionService } from 'src/role/services/role-permission.service';
import { UserRoleService } from 'src/role/services/user-role.service';
@Injectable()
export class PermissionGuard implements CanActivate {
@@ -11,13 +11,13 @@ export class PermissionGuard implements CanActivate {
private readonly userRoleService: UserRoleService,
private readonly rolePermissionService: RolePermissionService,
private readonly permissionService: PermissionService,
) { }
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const requiredPermissions = this.reflector.getAllAndOverride<string[]>('permissions', [
context.getHandler(),
context.getClass(),
]);
const requiredPermissions = this.reflector.getAllAndOverride<string[]>(
'permissions',
[context.getHandler(), context.getClass()],
);
if (!requiredPermissions) return true;
@@ -27,14 +27,21 @@ export class PermissionGuard implements CanActivate {
if (!userId) return false;
// 查询用户拥有的有效角色ID
const userRoleIds = await this.userRoleService.findValidRoleIdsByUserId(userId);
const userRoleIds =
await this.userRoleService.findValidRoleIdsByUserId(userId);
// 查询用户拥有的有效角色ID对应的权限ID
const userPermissionIds = await this.rolePermissionService.findPermissionIdsByRoleIds(userRoleIds);
const userPermissionIds =
await this.rolePermissionService.findPermissionIdsByRoleIds(userRoleIds);
// 查询用户拥有的权限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,7 +1,7 @@
import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common";
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { RoleService } from "src/role/services/role.service";
import { UserRoleService } from "src/role/services/user-role.service";
import { RoleService } from 'src/role/services/role.service';
import { UserRoleService } from 'src/role/services/user-role.service';
@Injectable()
export class RolesGuard implements CanActivate {
@@ -9,7 +9,7 @@ export class RolesGuard implements CanActivate {
private reflector: Reflector,
private readonly userRoleService: UserRoleService,
private readonly roleService: RoleService,
) { }
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const requiredRoles = this.reflector.getAllAndOverride<string[]>('roles', [
@@ -19,18 +19,19 @@ export class RolesGuard implements CanActivate {
if (!requiredRoles) return true;
const request = context.switchToHttp().getRequest();
const userId = request.user?.userId;
if (!userId) return false;
// 查询用户拥有的有效角色Id
const userRoleIds = await this.userRoleService.findValidRoleIdsByUserId(userId);
const userRoleIds =
await this.userRoleService.findValidRoleIdsByUserId(userId);
// 查询用户角色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,12 +1,20 @@
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from "@nestjs/common";
import { Observable } from "rxjs";
import { map } from "rxjs/operators";
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable()
export class ResponseInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler<any>): Observable<any> | Promise<Observable<any>> {
intercept(
context: ExecutionContext,
next: CallHandler<any>,
): Observable<any> | Promise<Observable<any>> {
return next.handle().pipe(
map(data => ({
map((data) => ({
statusCode: 200,
message: '请求成功',
data,

View File

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

View File

@@ -2,15 +2,19 @@ import { Injectable } from '@nestjs/common';
@Injectable()
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}`,
);
}
/**
* @deprecated 短信签名暂未通过
*/
async sendSMS(phone: string, type: 'login', code: string) {
throw new Error(
`SMS sending is not implemented yet. Phone: ${phone}, Type: ${type}, Code: ${code}`,
);
// const config = new $OpenApi.Config({
// accessKeyId: process.env.ALIYUN_ACCESS_KEY_ID,
// accessKeySecret: process.env.ALIYUN_ACCESS_KEY_SECRET,
@@ -28,7 +32,6 @@ export class NotificationService {
// }
// })();
// request.templateCode = code;
// await client.sendSms(request).then(a => {
// console.log(a)
// }).catch(err => {

View File

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

View File

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

View File

@@ -3,7 +3,6 @@ import { STS } from 'ali-oss';
@Injectable()
export class OssService {
private sts = new STS({
accessKeyId: process.env.ALIYUN_ACCESS_KEY_ID,
accessKeySecret: process.env.ALIYUN_ACCESS_KEY_SECRET,
@@ -18,7 +17,7 @@ export class OssService {
Expiration: string;
};
expireTime: number; // 时间戳,单位为毫秒
}
};
} = {};
/** @todo 该方法存在缓存穿透问题,待优化 */
@@ -34,20 +33,21 @@ export class OssService {
}
}
return this.sts.assumeRole(
process.env.ALIYUN_OSS_STS_ROLE_ARN, ``, 3600, `${session}`,
).then((res) => {
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分钟过期,
expireTime:
new Date(res.credentials.Expiration).getTime() - 5 * 60 * 1000, // 提前5分钟过期,
};
return res.credentials;
}).catch(err => {
})
.catch((err) => {
console.error('获取STS Token失败:', err);
throw new Error('获取STS Token失败');
})
});
}
}

View File

@@ -1,9 +1,16 @@
import { Column, CreateDateColumn, Entity, Index, PrimaryGeneratedColumn, UpdateDateColumn } from "typeorm";
import {
Column,
CreateDateColumn,
Entity,
Index,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
type ResourceTag = {
name: string;
type: string;
}
};
@Entity()
export class Resource {

View File

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

View File

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

View File

@@ -8,13 +8,13 @@ export class ResourceService {
constructor(
@InjectRepository(Resource)
private readonly resourceRepository: Repository<Resource>,
) { }
) {}
async findAll(): Promise<Resource[]> {
return this.resourceRepository.find({
order: {
createdAt: 'DESC',
}
},
});
}

View File

@@ -1,4 +1,4 @@
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class Permission {

View File

@@ -1,4 +1,4 @@
import { Entity, Index, PrimaryColumn } from "typeorm";
import { Entity, Index, PrimaryColumn } from 'typeorm';
@Entity()
@Index(['roleId', 'permissionId'], { unique: true })

View File

@@ -1,4 +1,4 @@
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class Role {

View File

@@ -1,4 +1,10 @@
import { Column, CreateDateColumn, Entity, Index, PrimaryGeneratedColumn } from "typeorm";
import {
Column,
CreateDateColumn,
Entity,
Index,
PrimaryGeneratedColumn,
} from 'typeorm';
@Entity()
@Index(['userId', 'roleId'])
@@ -10,7 +16,7 @@ export class UserRole {
roleId: string;
@Column('uuid')
userId: string
userId: string;
@Column()
isEnabled: boolean;

View File

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

View File

@@ -1,34 +1,38 @@
import { BadRequestException, Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Permission } from "../entities/permission.entity";
import { In, Repository } from "typeorm";
import { BadRequestException, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Permission } from '../entities/permission.entity';
import { In, Repository } from 'typeorm';
@Injectable()
export class PermissionService {
constructor(
@InjectRepository(Permission)
private readonly permissionRepository: Repository<Permission>,
) { }
) {}
async findPermissionNamesByPermissionIds(permissionIds: string[]): Promise<string[]> {
const permissions = await this.findPermissionsByPermissionIds(permissionIds);
return permissions.map(permission => permission.name);
async findPermissionNamesByPermissionIds(
permissionIds: string[],
): Promise<string[]> {
const permissions =
await this.findPermissionsByPermissionIds(permissionIds);
return permissions.map((permission) => permission.name);
}
async findPermissionsByPermissionIds(permissionIds: string[]): Promise<Permission[]> {
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),
}
},
});
}
@@ -36,13 +40,17 @@ export class PermissionService {
return this.permissionRepository.find();
}
async create(permission: Pick<Permission, 'name' | 'description'>): Promise<Permission> {
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 } });
const existingPermission = await this.permissionRepository.findOne({
where: { id: permissionId },
});
if (!existingPermission) {
throw new BadRequestException('Permission not found');
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,11 @@
import { Column, CreateDateColumn, DeleteDateColumn, Entity, Index, PrimaryGeneratedColumn } from "typeorm";
import {
Column,
CreateDateColumn,
DeleteDateColumn,
Entity,
Index,
PrimaryGeneratedColumn,
} from 'typeorm';
@Entity()
@Index(['sessionId', 'userId'])

View File

@@ -1,11 +1,26 @@
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';
@Entity()
@Index("IDX_user_userid", ["userId"], { unique: true })
@Index("IDX_user_username", ["username"], { unique: true })
@Index("IDX_user_email", ["email"], { unique: true, where: "email IS NOT NULL" })
@Index("IDX_user_phone", ["phone"], { unique: true, where: "phone IS NOT NULL" })
@Index('IDX_user_userid', ['userId'], { unique: true })
@Index('IDX_user_username', ['username'], { unique: true })
@Index('IDX_user_email', ['email'], {
unique: true,
where: 'email IS NOT NULL',
})
@Index('IDX_user_phone', ['phone'], {
unique: true,
where: 'phone IS NOT NULL',
})
export class User {
@PrimaryGeneratedColumn('uuid')
userId: string;
@@ -38,8 +53,8 @@ export class User {
transformer: {
to: (value: string | null) => value?.trim() || null,
from: (value: string | null) => value,
}
})// RFC 5321
},
}) // RFC 5321
email: string | null;
@Column({
@@ -48,8 +63,8 @@ export class User {
transformer: {
to: (value: string | null) => value?.trim() || null,
from: (value: string | null) => value,
}
})// China Mainland
},
}) // China Mainland
phone: string | null;
@Column({
@@ -57,7 +72,7 @@ export class User {
transformer: {
to: (value: string | null) => value?.trim() || null,
from: (value: string | null) => value,
}
},
})
avatar: string;

View File

@@ -1,15 +1,14 @@
import { InjectRepository } from "@nestjs/typeorm";
import { Injectable } from "@nestjs/common";
import { UserSession } from "../entities/user-session.entity";
import { Repository } from "typeorm";
import { InjectRepository } from '@nestjs/typeorm';
import { Injectable } from '@nestjs/common';
import { UserSession } from '../entities/user-session.entity';
import { Repository } from 'typeorm';
@Injectable()
export class UserSessionService {
constructor(
@InjectRepository(UserSession)
private readonly userSessionRepository: Repository<UserSession>,
) { }
) {}
async createSession(userId: string, sessionId: string): Promise<UserSession> {
const session = this.userSessionRepository.create({
@@ -25,7 +24,7 @@ export class UserSessionService {
userId,
sessionId,
deletedAt: null,
}
},
});
return !!session;
@@ -37,7 +36,7 @@ export class UserSessionService {
userId,
sessionId,
deletedAt: null,
}
},
});
if (session) {

View File

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

View File

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

View File

@@ -1,20 +1,29 @@
import { BadRequestException, ConflictException, Injectable } from '@nestjs/common';
import {
BadRequestException,
ConflictException,
Injectable,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { User } from './entities/user.entity';
import { QueryFailedError, Repository } from 'typeorm';
import { createHash, ECDH } from 'crypto';
import { createHash } from 'crypto';
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()
export class UserService {
constructor(
@InjectRepository(User)
private readonly userRepository: Repository<User>,
) { }
) {}
async findOne(options: UserFindOptions | UserFindOptions[], additionalOptions?: { withDeleted?: boolean }): Promise<User | null> {
async findOne(
options: UserFindOptions | UserFindOptions[],
additionalOptions?: { withDeleted?: boolean },
): Promise<User | null> {
if (Object.keys(options).length === 0) {
throw new BadRequestException('查询条件不能为空');
}
@@ -37,7 +46,9 @@ export class UserService {
}
async update(userId: string, user: Partial<User>): Promise<User> {
const existingUser = await this.userRepository.findOne({ where: { userId } });
const existingUser = await this.userRepository.findOne({
where: { userId },
});
if (!existingUser) {
throw new BadRequestException('User not found');
}
@@ -52,22 +63,25 @@ export class UserService {
}
async delete(userId: string, soft: boolean) {
const existingUser = await this.userRepository.findOne({ where: { userId }, withDeleted: true });
const existingUser = await this.userRepository.findOne({
where: { userId },
withDeleted: true,
});
if (!existingUser) {
throw new BadRequestException('用户不存在');
}
if (existingUser.deletedAt && soft) {
throw new BadRequestException('账户已注销,不得重复操作')
throw new BadRequestException('账户已注销,不得重复操作');
}
if (!existingUser.deletedAt && !soft) {
throw new BadRequestException('账号未注销,请先注销再执行删除操作')
throw new BadRequestException('账号未注销,请先注销再执行删除操作');
}
return soft
? await this.userRepository.softDelete(existingUser.userId)
: await this.userRepository.delete(existingUser.userId)
: await this.userRepository.delete(existingUser.userId);
}
hashPassword(password: string, salt: string): string {
@@ -104,7 +118,7 @@ export class UserService {
}
async list(page = 1, pageSize = 20) {
const queryBuilder = this.userRepository.createQueryBuilder('user')
const queryBuilder = this.userRepository.createQueryBuilder('user');
queryBuilder.withDeleted();
@@ -119,6 +133,6 @@ export class UserService {
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 {
@IsEnum(['phone', 'email'], { message: '请求类型错误' })
targetType: 'phone' | 'email';
@IsEnum(['login'], { message: '请求类型错误' })
type: 'login'
type: 'login';
@ValidateIf(o => o.targetType === 'phone')
@ValidateIf((o) => o.targetType === 'phone')
@IsString({ message: '手机号必须输入' })
@Length(11, 11, { message: '手机号异常' })// 中国大陆11位数字
@Length(11, 11, { message: '手机号异常' }) // 中国大陆11位数字
phone?: string;
@ValidateIf(o => o.targetType === 'email')
@ValidateIf((o) => o.targetType === 'email')
@IsString({ message: '邮箱必须输入' })
@Length(6, 254, { message: '邮箱异常' })// RFC 5321
@Length(6, 254, { message: '邮箱异常' }) // RFC 5321
email?: string;
}

View File

@@ -4,10 +4,7 @@ import { VerificationService } from './verification.service';
@Controller('verification')
export class VerificationController {
constructor(
private readonly verificationService: VerificationService,
) { }
constructor(private readonly verificationService: VerificationService) {}
@Post('send')
async sendVerificationCode(@Body() dto: SendVerificationCodeDto) {

View File

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

View File

@@ -3,20 +3,20 @@ import { NotificationService } from 'src/notification/notification.service';
@Injectable()
export class VerificationService {
private readonly logger = new Logger(VerificationService.name);
constructor(
private readonly notificationService: NotificationService,
) { }
constructor(private readonly notificationService: NotificationService) {}
private pool: Map<string, {
private pool: Map<
string,
{
code: string;
createdAt: number;
expiredAt: number;
tryCount: number;
maxTryCount: number;
}> = new Map();
}
> = new Map();
async sendPhoneCode(phone: string, type: 'login') {
const key = `phone:${phone}:${type}`;
@@ -96,4 +96,3 @@ export class VerificationService {
return Math.floor(100000 + Math.random() * 900000).toString();
}
}