chore(deps): add zod and nestjs-zod for schema validation

This commit is contained in:
2026-02-07 03:43:59 +08:00
parent 0bf46e887d
commit e8a8ad389c
7 changed files with 175 additions and 146 deletions

View File

@@ -32,16 +32,15 @@
"@prisma/client": "^7.3.0", "@prisma/client": "^7.3.0",
"bcryptjs": "^3.0.3", "bcryptjs": "^3.0.3",
"better-sqlite3": "^12.6.2", "better-sqlite3": "^12.6.2",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.3",
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"jsonwebtoken": "^9.0.2", "nestjs-zod": "^5.1.1",
"passport": "^0.7.0", "passport": "^0.7.0",
"passport-jwt": "^4.0.1", "passport-jwt": "^4.0.1",
"prisma": "^7.3.0", "prisma": "^7.3.0",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"reflect-metadata": "^0.2.2", "reflect-metadata": "^0.2.2",
"rxjs": "^7.8.2" "rxjs": "^7.8.2",
"zod": "^4.3.6"
}, },
"devDependencies": { "devDependencies": {
"@nestjs/cli": "^11.0.16", "@nestjs/cli": "^11.0.16",
@@ -49,7 +48,6 @@
"@nestjs/testing": "^11.1.13", "@nestjs/testing": "^11.1.13",
"@types/express": "^5.0.6", "@types/express": "^5.0.6",
"@types/jest": "^30.0.0", "@types/jest": "^30.0.0",
"@types/jsonwebtoken": "^9.0.10",
"@types/node": "^25.2.1", "@types/node": "^25.2.1",
"@types/passport-jwt": "^4.0.1", "@types/passport-jwt": "^4.0.1",
"@types/qrcode": "^1.5.6", "@types/qrcode": "^1.5.6",

54
pnpm-lock.yaml generated
View File

@@ -41,18 +41,12 @@ importers:
better-sqlite3: better-sqlite3:
specifier: ^12.6.2 specifier: ^12.6.2
version: 12.6.2 version: 12.6.2
class-transformer:
specifier: ^0.5.1
version: 0.5.1
class-validator:
specifier: ^0.14.3
version: 0.14.3
dotenv: dotenv:
specifier: ^17.2.3 specifier: ^17.2.3
version: 17.2.4 version: 17.2.4
jsonwebtoken: nestjs-zod:
specifier: ^9.0.2 specifier: ^5.1.1
version: 9.0.3 version: 5.1.1(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2)(zod@4.3.6)
passport: passport:
specifier: ^0.7.0 specifier: ^0.7.0
version: 0.7.0 version: 0.7.0
@@ -71,6 +65,9 @@ importers:
rxjs: rxjs:
specifier: ^7.8.2 specifier: ^7.8.2
version: 7.8.2 version: 7.8.2
zod:
specifier: ^4.3.6
version: 4.3.6
devDependencies: devDependencies:
'@nestjs/cli': '@nestjs/cli':
specifier: ^11.0.16 specifier: ^11.0.16
@@ -87,9 +84,6 @@ importers:
'@types/jest': '@types/jest':
specifier: ^30.0.0 specifier: ^30.0.0
version: 30.0.0 version: 30.0.0
'@types/jsonwebtoken':
specifier: ^9.0.10
version: 9.0.10
'@types/node': '@types/node':
specifier: ^25.2.1 specifier: ^25.2.1
version: 25.2.1 version: 25.2.1
@@ -2601,6 +2595,17 @@ packages:
neo-async@2.6.2: neo-async@2.6.2:
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
nestjs-zod@5.1.1:
resolution: {integrity: sha512-pXa9Jrdip7iedKvGxJTvvCFVRCoIcNENPCsHjpCefPH3PcFejRgkZkUcr3TYITRyxnUk7Zy5OsLpirZGLYBfBQ==}
peerDependencies:
'@nestjs/common': ^10.0.0 || ^11.0.0
'@nestjs/swagger': ^7.4.2 || ^8.0.0 || ^11.0.0
rxjs: ^7.0.0
zod: ^3.25.0 || ^4.0.0
peerDependenciesMeta:
'@nestjs/swagger':
optional: true
node-abi@3.87.0: node-abi@3.87.0:
resolution: {integrity: sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==} resolution: {integrity: sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
@@ -3439,6 +3444,9 @@ packages:
zeptomatch@2.1.0: zeptomatch@2.1.0:
resolution: {integrity: sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA==} resolution: {integrity: sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA==}
zod@4.3.6:
resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==}
snapshots: snapshots:
'@angular-devkit/core@19.2.17(chokidar@4.0.3)': '@angular-devkit/core@19.2.17(chokidar@4.0.3)':
@@ -4587,7 +4595,8 @@ snapshots:
'@types/methods': 1.1.4 '@types/methods': 1.1.4
'@types/superagent': 8.1.9 '@types/superagent': 8.1.9
'@types/validator@13.15.10': {} '@types/validator@13.15.10':
optional: true
'@types/yargs-parser@21.0.3': {} '@types/yargs-parser@21.0.3': {}
@@ -5027,13 +5036,15 @@ snapshots:
cjs-module-lexer@2.2.0: {} cjs-module-lexer@2.2.0: {}
class-transformer@0.5.1: {} class-transformer@0.5.1:
optional: true
class-validator@0.14.3: class-validator@0.14.3:
dependencies: dependencies:
'@types/validator': 13.15.10 '@types/validator': 13.15.10
libphonenumber-js: 1.12.36 libphonenumber-js: 1.12.36
validator: 13.15.26 validator: 13.15.26
optional: true
cli-cursor@3.1.0: cli-cursor@3.1.0:
dependencies: dependencies:
@@ -6055,7 +6066,8 @@ snapshots:
leven@3.1.0: {} leven@3.1.0: {}
libphonenumber-js@1.12.36: {} libphonenumber-js@1.12.36:
optional: true
lilconfig@2.1.0: {} lilconfig@2.1.0: {}
@@ -6221,6 +6233,13 @@ snapshots:
neo-async@2.6.2: {} neo-async@2.6.2: {}
nestjs-zod@5.1.1(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2)(zod@4.3.6):
dependencies:
'@nestjs/common': 11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2)
deepmerge: 4.3.1
rxjs: 7.8.2
zod: 4.3.6
node-abi@3.87.0: node-abi@3.87.0:
dependencies: dependencies:
semver: 7.7.4 semver: 7.7.4
@@ -6964,7 +6983,8 @@ snapshots:
optionalDependencies: optionalDependencies:
typescript: 5.9.3 typescript: 5.9.3
validator@13.15.26: {} validator@13.15.26:
optional: true
vary@1.1.2: {} vary@1.1.2: {}
@@ -7099,3 +7119,5 @@ snapshots:
dependencies: dependencies:
grammex: 3.1.12 grammex: 3.1.12
graphmatch: 1.1.0 graphmatch: 1.1.0
zod@4.3.6: {}

View File

@@ -1,29 +1,45 @@
import { Controller, Post, Get, Put, Body, UseGuards, Request, HttpCode, HttpStatus } from '@nestjs/common'; import {
import { AuthService } from './auth.service'; Controller,
import { AuthGuard } from './auth.guard'; Post,
import { LoginDto, ChangePasswordDto, UpdateProfileDto } from './dto'; Get,
Put,
Body,
UseGuards,
Request,
HttpCode,
HttpStatus,
} from "@nestjs/common";
import { AuthService } from "./auth.service";
import { AuthGuard } from "./auth.guard";
import { LoginDto, ChangePasswordDto, UpdateProfileDto } from "./dto";
@Controller('auth') @Controller("auth")
export class AuthController { export class AuthController {
constructor(private authService: AuthService) {} constructor(private authService: AuthService) {}
@Post('login') @Post("login")
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
async login(@Body() loginDto: LoginDto) { async login(@Body(LoginDto) loginDto: any) {
const user = await this.authService.validateUser(loginDto.username, loginDto.password); const user = await this.authService.validateUser(
loginDto.username,
loginDto.password,
);
return this.authService.login(user); return this.authService.login(user);
} }
@Get('profile') @Get("profile")
@UseGuards(AuthGuard) @UseGuards(AuthGuard)
async getProfile(@Request() req) { async getProfile(@Request() req) {
return this.authService.getProfile(req.user.id); return this.authService.getProfile(req.user.id);
} }
@Post('change-password') @Post("change-password")
@UseGuards(AuthGuard) @UseGuards(AuthGuard)
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
async changePassword(@Request() req, @Body() changePasswordDto: ChangePasswordDto) { async changePassword(
@Request() req,
@Body(ChangePasswordDto) changePasswordDto: any,
) {
return this.authService.changePassword( return this.authService.changePassword(
req.user.id, req.user.id,
changePasswordDto.currentPassword, changePasswordDto.currentPassword,
@@ -31,10 +47,17 @@ export class AuthController {
); );
} }
@Put('profile') @Put("profile")
@UseGuards(AuthGuard) @UseGuards(AuthGuard)
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
async updateProfile(@Request() req, @Body() updateProfileDto: UpdateProfileDto) { async updateProfile(
return this.authService.updateProfile(req.user.id, updateProfileDto.name, updateProfileDto.email); @Request() req,
@Body(UpdateProfileDto) updateProfileDto: any,
) {
return this.authService.updateProfile(
req.user.id,
updateProfileDto.name,
updateProfileDto.email,
);
} }
} }

View File

@@ -1,31 +1,16 @@
import { IsString, IsNotEmpty, MinLength, IsEmail, IsOptional } from 'class-validator'; import { z } from "zod";
export class LoginDto { export const LoginDto = z.object({
@IsString() username: z.string().min(1, "用户名不能为空"),
@IsNotEmpty({ message: '用户名不能为空' }) password: z.string().min(1, "密码不能为空"),
username: string; });
@IsString() export const ChangePasswordDto = z.object({
@IsNotEmpty({ message: '密码不能为空' }) currentPassword: z.string().min(1, "当前密码不能为空"),
password: string; newPassword: z.string().min(6, "新密码长度至少为6位"),
} });
export class ChangePasswordDto { export const UpdateProfileDto = z.object({
@IsString() name: z.string().min(1, "姓名不能为空"),
@IsNotEmpty({ message: '当前密码不能为空' }) email: z.string().email("邮箱格式不正确").optional(),
currentPassword: string; });
@IsString()
@MinLength(6, { message: '新密码长度至少为6位' })
newPassword: string;
}
export class UpdateProfileDto {
@IsString()
@IsNotEmpty({ message: '姓名不能为空' })
name: string;
@IsOptional()
@IsEmail({}, { message: '邮箱格式不正确' })
email?: string;
}

View File

@@ -1,34 +1,34 @@
import { NestFactory } from '@nestjs/core'; import { NestFactory } from "@nestjs/core";
import { AppModule } from './app.module'; import { AppModule } from "./app.module";
import { ValidationPipe } from '@nestjs/common'; import { ConfigService } from "@nestjs/config";
import { ConfigService } from '@nestjs/config'; import { join } from "path";
import { join } from 'path'; import { NestExpressApplication } from "@nestjs/platform-express";
import { NestExpressApplication } from '@nestjs/platform-express'; import { ZodValidationPipe } from "nestjs-zod";
async function bootstrap() { async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule); const app = await NestFactory.create<NestExpressApplication>(AppModule);
const configService = app.get(ConfigService); const configService = app.get(ConfigService);
app.enableCors(); app.enableCors();
app.useGlobalPipes(new ValidationPipe({ transform: true })); app.useGlobalPipes(new ZodValidationPipe());
app.setGlobalPrefix('api'); app.setGlobalPrefix("api");
// 静态文件服务 // 静态文件服务
const frontendPath = join(__dirname, '..', 'frontend'); const frontendPath = join(__dirname, "..", "frontend");
const distPath = join(frontendPath, 'dist'); const distPath = join(frontendPath, "dist");
const publicPath = join(frontendPath, 'public'); const publicPath = join(frontendPath, "public");
if (process.env.NODE_ENV === 'production') { if (process.env.NODE_ENV === "production") {
app.useStaticAssets(distPath); app.useStaticAssets(distPath);
} else { } else {
app.useStaticAssets(publicPath); app.useStaticAssets(publicPath);
} }
const port = configService.get<number>('PORT', 3000); const port = configService.get<number>("PORT", 3000);
await app.listen(port); await app.listen(port);
console.log(`服务器运行在 http://localhost:${port}`); console.log(`服务器运行在 http://localhost:${port}`);
console.log(`API文档: http://localhost:${port}/api/health`); console.log(`API文档: http://localhost:${port}/api/health`);
console.log(`环境: ${process.env.NODE_ENV || 'development'}`); console.log(`环境: ${process.env.NODE_ENV || "development"}`);
} }
bootstrap(); bootstrap();

View File

@@ -1,46 +1,24 @@
import { IsString, IsNotEmpty, IsNumber, IsOptional, IsBoolean, Min, Max } from 'class-validator'; import { z } from "zod";
export class GenerateSerialDto { export const GenerateSerialDto = z.object({
@IsString() companyName: z.string().min(1, "企业名称不能为空"),
@IsNotEmpty({ message: '企业名称不能为空' }) quantity: z.number().min(1).max(100).optional(),
companyName: string; validDays: z.number().optional(),
});
@IsOptional() export const GenerateWithPrefixDto = GenerateSerialDto.extend({
@IsNumber() serialPrefix: z.string().min(1, "自定义前缀不能为空"),
@Min(1) });
@Max(100)
quantity?: number;
@IsOptional() export const QRCodeDto = z.object({
@IsNumber() baseUrl: z.string().optional(),
validDays?: number; });
}
export class GenerateWithPrefixDto extends GenerateSerialDto { export const UpdateSerialDto = z.object({
@IsString() companyName: z.string().optional(),
@IsNotEmpty({ message: '自定义前缀不能为空' }) validUntil: z.string().optional(),
serialPrefix: string; isActive: z.boolean().optional(),
} });
export class QRCodeDto {
@IsOptional()
@IsString()
baseUrl?: string;
}
export class UpdateSerialDto {
@IsOptional()
@IsString()
companyName?: string;
@IsOptional()
@IsString()
validUntil?: string;
@IsOptional()
@IsBoolean()
isActive?: boolean;
}
export interface Serial { export interface Serial {
id: number; id: number;

View File

@@ -1,18 +1,38 @@
import { Controller, Get, Post, Patch, Body, Param, Query, UseGuards, Req, HttpCode, HttpStatus } from '@nestjs/common'; import {
import { Request } from 'express'; Controller,
import { SerialsService } from './serials.service'; Get,
import { AuthGuard } from '../auth/auth.guard'; Post,
import { AdminGuard } from '../auth/admin.guard'; Patch,
import { GenerateSerialDto, GenerateWithPrefixDto, QRCodeDto, UpdateSerialDto } from './dto'; Body,
Param,
Query,
UseGuards,
Req,
HttpCode,
HttpStatus,
} from "@nestjs/common";
import { Request } from "express";
import { SerialsService } from "./serials.service";
import { AuthGuard } from "../auth/auth.guard";
import { AdminGuard } from "../auth/admin.guard";
import {
GenerateSerialDto,
GenerateWithPrefixDto,
QRCodeDto,
UpdateSerialDto,
} from "./dto";
@Controller('serials') @Controller("serials")
export class SerialsController { export class SerialsController {
constructor(private readonly serialsService: SerialsService) {} constructor(private readonly serialsService: SerialsService) {}
@Post('generate') @Post("generate")
@UseGuards(AuthGuard, AdminGuard) @UseGuards(AuthGuard, AdminGuard)
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
async generate(@Body() generateDto: GenerateSerialDto, @Req() req: Request) { async generate(
@Body(GenerateSerialDto) generateDto: any,
@Req() req: Request,
) {
const serials = await this.serialsService.generate( const serials = await this.serialsService.generate(
generateDto.companyName, generateDto.companyName,
generateDto.quantity || 1, generateDto.quantity || 1,
@@ -25,10 +45,13 @@ export class SerialsController {
}; };
} }
@Post('generate-with-prefix') @Post("generate-with-prefix")
@UseGuards(AuthGuard, AdminGuard) @UseGuards(AuthGuard, AdminGuard)
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
async generateWithPrefix(@Body() generateDto: GenerateWithPrefixDto, @Req() req: Request) { async generateWithPrefix(
@Body(GenerateWithPrefixDto) generateDto: any,
@Req() req: Request,
) {
const serials = await this.serialsService.generate( const serials = await this.serialsService.generate(
generateDto.companyName, generateDto.companyName,
generateDto.quantity || 1, generateDto.quantity || 1,
@@ -42,44 +65,44 @@ export class SerialsController {
}; };
} }
@Post(':serialNumber/qrcode') @Post(":serialNumber/qrcode")
@UseGuards(AuthGuard) @UseGuards(AuthGuard)
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
async generateQRCode( async generateQRCode(
@Param('serialNumber') serialNumber: string, @Param("serialNumber") serialNumber: string,
@Body() qrCodeDto: QRCodeDto, @Body(QRCodeDto) qrCodeDto: any,
@Req() req: Request, @Req() req: Request,
) { ) {
return this.serialsService.generateQRCode( return this.serialsService.generateQRCode(
serialNumber, serialNumber,
qrCodeDto.baseUrl, qrCodeDto.baseUrl,
req.get('host'), req.get("host"),
req.protocol, req.protocol,
); );
} }
@Get(':serialNumber/query') @Get(":serialNumber/query")
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
async query(@Param('serialNumber') serialNumber: string) { async query(@Param("serialNumber") serialNumber: string) {
return this.serialsService.query(serialNumber); return this.serialsService.query(serialNumber);
} }
@Get() @Get()
@UseGuards(AuthGuard) @UseGuards(AuthGuard)
async findAll( async findAll(
@Query('page') page: string = '1', @Query("page") page: string = "1",
@Query('limit') limit: string = '20', @Query("limit") limit: string = "20",
@Query('search') search: string = '', @Query("search") search: string = "",
) { ) {
return this.serialsService.findAll(parseInt(page), parseInt(limit), search); return this.serialsService.findAll(parseInt(page), parseInt(limit), search);
} }
@Patch(':serialNumber') @Patch(":serialNumber")
@UseGuards(AuthGuard, AdminGuard) @UseGuards(AuthGuard, AdminGuard)
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
async update( async update(
@Param('serialNumber') serialNumber: string, @Param("serialNumber") serialNumber: string,
@Body() updateDto: UpdateSerialDto, @Body(UpdateSerialDto) updateDto: any,
) { ) {
return this.serialsService.update(serialNumber, { return this.serialsService.update(serialNumber, {
companyName: updateDto.companyName, companyName: updateDto.companyName,
@@ -88,10 +111,10 @@ export class SerialsController {
}); });
} }
@Post(':serialNumber/revoke') @Post(":serialNumber/revoke")
@UseGuards(AuthGuard, AdminGuard) @UseGuards(AuthGuard, AdminGuard)
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
async revoke(@Param('serialNumber') serialNumber: string) { async revoke(@Param("serialNumber") serialNumber: string) {
return this.serialsService.revoke(serialNumber); return this.serialsService.revoke(serialNumber);
} }
} }