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

@@ -1,29 +1,45 @@
import { Controller, Post, 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';
import {
Controller,
Post,
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 {
constructor(private authService: AuthService) {}
@Post('login')
@Post("login")
@HttpCode(HttpStatus.OK)
async login(@Body() loginDto: LoginDto) {
const user = await this.authService.validateUser(loginDto.username, loginDto.password);
async login(@Body(LoginDto) loginDto: any) {
const user = await this.authService.validateUser(
loginDto.username,
loginDto.password,
);
return this.authService.login(user);
}
@Get('profile')
@Get("profile")
@UseGuards(AuthGuard)
async getProfile(@Request() req) {
return this.authService.getProfile(req.user.id);
}
@Post('change-password')
@Post("change-password")
@UseGuards(AuthGuard)
@HttpCode(HttpStatus.OK)
async changePassword(@Request() req, @Body() changePasswordDto: ChangePasswordDto) {
async changePassword(
@Request() req,
@Body(ChangePasswordDto) changePasswordDto: any,
) {
return this.authService.changePassword(
req.user.id,
changePasswordDto.currentPassword,
@@ -31,10 +47,17 @@ export class AuthController {
);
}
@Put('profile')
@Put("profile")
@UseGuards(AuthGuard)
@HttpCode(HttpStatus.OK)
async updateProfile(@Request() req, @Body() updateProfileDto: UpdateProfileDto) {
return this.authService.updateProfile(req.user.id, updateProfileDto.name, updateProfileDto.email);
async updateProfile(
@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 {
@IsString()
@IsNotEmpty({ message: '用户名不能为空' })
username: string;
export const LoginDto = z.object({
username: z.string().min(1, "用户名不能为空"),
password: z.string().min(1, "密码不能为空"),
});
@IsString()
@IsNotEmpty({ message: '密码不能为空' })
password: string;
}
export const ChangePasswordDto = z.object({
currentPassword: z.string().min(1, "当前密码不能为空"),
newPassword: z.string().min(6, "新密码长度至少为6位"),
});
export class ChangePasswordDto {
@IsString()
@IsNotEmpty({ message: '当前密码不能为空' })
currentPassword: string;
@IsString()
@MinLength(6, { message: '新密码长度至少为6位' })
newPassword: string;
}
export class UpdateProfileDto {
@IsString()
@IsNotEmpty({ message: '姓名不能为空' })
name: string;
@IsOptional()
@IsEmail({}, { message: '邮箱格式不正确' })
email?: string;
}
export const UpdateProfileDto = z.object({
name: z.string().min(1, "姓名不能为空"),
email: z.string().email("邮箱格式不正确").optional(),
});

View File

@@ -1,34 +1,34 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { join } from 'path';
import { NestExpressApplication } from '@nestjs/platform-express';
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
import { ConfigService } from "@nestjs/config";
import { join } from "path";
import { NestExpressApplication } from "@nestjs/platform-express";
import { ZodValidationPipe } from "nestjs-zod";
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule);
const configService = app.get(ConfigService);
app.enableCors();
app.useGlobalPipes(new ValidationPipe({ transform: true }));
app.setGlobalPrefix('api');
app.useGlobalPipes(new ZodValidationPipe());
app.setGlobalPrefix("api");
// 静态文件服务
const frontendPath = join(__dirname, '..', 'frontend');
const distPath = join(frontendPath, 'dist');
const publicPath = join(frontendPath, 'public');
if (process.env.NODE_ENV === 'production') {
const frontendPath = join(__dirname, "..", "frontend");
const distPath = join(frontendPath, "dist");
const publicPath = join(frontendPath, "public");
if (process.env.NODE_ENV === "production") {
app.useStaticAssets(distPath);
} else {
app.useStaticAssets(publicPath);
}
const port = configService.get<number>('PORT', 3000);
const port = configService.get<number>("PORT", 3000);
await app.listen(port);
console.log(`服务器运行在 http://localhost:${port}`);
console.log(`API文档: http://localhost:${port}/api/health`);
console.log(`环境: ${process.env.NODE_ENV || 'development'}`);
console.log(`环境: ${process.env.NODE_ENV || "development"}`);
}
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 {
@IsString()
@IsNotEmpty({ message: '企业名称不能为空' })
companyName: string;
export const GenerateSerialDto = z.object({
companyName: z.string().min(1, "企业名称不能为空"),
quantity: z.number().min(1).max(100).optional(),
validDays: z.number().optional(),
});
@IsOptional()
@IsNumber()
@Min(1)
@Max(100)
quantity?: number;
export const GenerateWithPrefixDto = GenerateSerialDto.extend({
serialPrefix: z.string().min(1, "自定义前缀不能为空"),
});
@IsOptional()
@IsNumber()
validDays?: number;
}
export const QRCodeDto = z.object({
baseUrl: z.string().optional(),
});
export class GenerateWithPrefixDto extends GenerateSerialDto {
@IsString()
@IsNotEmpty({ message: '自定义前缀不能为空' })
serialPrefix: string;
}
export class QRCodeDto {
@IsOptional()
@IsString()
baseUrl?: string;
}
export class UpdateSerialDto {
@IsOptional()
@IsString()
companyName?: string;
@IsOptional()
@IsString()
validUntil?: string;
@IsOptional()
@IsBoolean()
isActive?: boolean;
}
export const UpdateSerialDto = z.object({
companyName: z.string().optional(),
validUntil: z.string().optional(),
isActive: z.boolean().optional(),
});
export interface Serial {
id: number;

View File

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