feat(deploy): production deployment setup and fixes

- Debian-based api image (bookworm-slim), docker/debian mirrors, prisma
  binaryTargets for openssl 3.0
- nginx: admin SPA under /admin, TLS via acme.sh (ZeroSSL) + auto-renewal
  cron, http->https redirect
- prisma: add origin_goods.delisted migration, sync missing schema
  (good_image/tag_font_color/good_tags), fix users.createdAt Timestamptz
- api: CORS wildcard reflection, helmet CORP cross-origin, price
  backfill in persistProductDetail, categoryIcon ancestor fallback,
  mediaByColor per-color gallery in public goods detail
- admin: /admin base path (vite + router)
- import-data.mjs: udt_name casting, serial sequence advance fix
This commit is contained in:
yeuimu
2026-08-26 14:23:09 +08:00
parent be0b90e68f
commit 6c61a4e871
982 changed files with 74156 additions and 179393 deletions
+54 -54
View File
@@ -1,54 +1,54 @@
import {
Controller,
Post,
UseGuards,
UseInterceptors,
UploadedFile,
BadRequestException,
} from '@nestjs/common';
import { Throttle } from '@nestjs/throttler';
import { FileInterceptor } from '@nestjs/platform-express';
import { diskStorage } from 'multer';
import { extname, join } from 'path';
import { randomUUID } from 'crypto';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
const UPLOAD_DIR = join(process.cwd(), 'uploads');
// Explicit safe-image whitelist. SVG is deliberately excluded: it can
// carry scripts and is served from the same origin (stored XSS).
const ALLOWED_EXTENSIONS = /\.(png|jpe?g|webp|gif)$/i;
const ALLOWED_MIMETYPES = /^image\/(png|jpe?g|webp|gif)$/i;
@UseGuards(JwtAuthGuard)
@Controller('upload')
export class UploadController {
@Post('image')
@Throttle({ default: { limit: 10, ttl: 60_000 } })
@UseInterceptors(
FileInterceptor('file', {
storage: diskStorage({
destination: UPLOAD_DIR,
filename: (_req, file, cb) => {
const ext = ALLOWED_EXTENSIONS.test(extname(file.originalname))
? extname(file.originalname).toLowerCase()
: '.png';
cb(null, `${randomUUID()}${ext}`);
},
}),
limits: { fileSize: 5 * 1024 * 1024 },
fileFilter: (_req, file, cb) => {
if (!ALLOWED_EXTENSIONS.test(file.originalname) || !ALLOWED_MIMETYPES.test(file.mimetype)) {
return cb(new BadRequestException('仅支持 png/jpg/webp/gif 图片'), false);
}
cb(null, true);
},
}),
)
uploadImage(@UploadedFile() file: Express.Multer.File) {
if (!file) {
throw new BadRequestException('请选择要上传的文件');
}
return { url: `/uploads/${file.filename}`, filename: file.filename };
}
}
import {
Controller,
Post,
UseGuards,
UseInterceptors,
UploadedFile,
BadRequestException,
} from '@nestjs/common';
import { Throttle } from '@nestjs/throttler';
import { FileInterceptor } from '@nestjs/platform-express';
import { diskStorage } from 'multer';
import { extname, join } from 'path';
import { randomUUID } from 'crypto';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
const UPLOAD_DIR = join(process.cwd(), 'uploads');
// Explicit safe-image whitelist. SVG is deliberately excluded: it can
// carry scripts and is served from the same origin (stored XSS).
const ALLOWED_EXTENSIONS = /\.(png|jpe?g|webp|gif)$/i;
const ALLOWED_MIMETYPES = /^image\/(png|jpe?g|webp|gif)$/i;
@UseGuards(JwtAuthGuard)
@Controller('upload')
export class UploadController {
@Post('image')
@Throttle({ default: { limit: 10, ttl: 60_000 } })
@UseInterceptors(
FileInterceptor('file', {
storage: diskStorage({
destination: UPLOAD_DIR,
filename: (_req, file, cb) => {
const ext = ALLOWED_EXTENSIONS.test(extname(file.originalname))
? extname(file.originalname).toLowerCase()
: '.png';
cb(null, `${randomUUID()}${ext}`);
},
}),
limits: { fileSize: 5 * 1024 * 1024 },
fileFilter: (_req, file, cb) => {
if (!ALLOWED_EXTENSIONS.test(file.originalname) || !ALLOWED_MIMETYPES.test(file.mimetype)) {
return cb(new BadRequestException('仅支持 png/jpg/webp/gif 图片'), false);
}
cb(null, true);
},
}),
)
uploadImage(@UploadedFile() file: Express.Multer.File) {
if (!file) {
throw new BadRequestException('请选择要上传的文件');
}
return { url: `/uploads/${file.filename}`, filename: file.filename };
}
}