format + lint
This commit is contained in:
@@ -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 {}
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 {}
|
||||||
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,4 +42,4 @@ import { OssModule } from './oss/oss.module';
|
|||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [AppService],
|
providers: [AppService],
|
||||||
})
|
})
|
||||||
export class AppModule { }
|
export class AppModule {}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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; // 如果有用户信息,返回用户对象
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {}
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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),
|
||||||
}
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
})),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
}
|
||||||
// })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {}
|
||||||
|
|||||||
@@ -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失败');
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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],
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {}
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {}
|
||||||
|
|||||||
@@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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('不支持的验证码类型');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {}
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user