AI Implementation feature(885): Admin Area General Settings and Categories 1.03 (#25)

This commit was merged in pull request #25.
This commit is contained in:
2026-07-22 13:06:34 +00:00
parent b7e0f3c608
commit b2f0a4736d
7 changed files with 157 additions and 152 deletions
@@ -4,7 +4,7 @@ import { FileInterceptor } from '@nestjs/platform-express';
import { ConfigService } from '@nestjs/config';
import * as fs from 'fs';
import * as path from 'path';
import sharp from 'sharp';
import sharp, { FormatEnum } from 'sharp';
import { AdminGuard } from '../../common/guards/admin.guard';
import { Roles } from '../../common/decorators/roles.decorator';
import { parseUploadSizeLimit, safeFilename } from '../../common/utils/upload';
@@ -50,6 +50,21 @@ export class UploadsController {
return this.handleLogoUpload(req);
}
/**
* Allowed image formats for the site logo. Detection is performed by
* decoding the full payload with sharp — never by trusting the
* multipart `mimetype` header, the original filename extension, or
* the browser's `accept="image/*"` filter.
*/
private static readonly ALLOWED_LOGO_FORMATS: ReadonlySet<keyof FormatEnum> = new Set<keyof FormatEnum>([
'png',
'jpeg',
'gif',
'webp',
]);
private static readonly LOGO_ERROR_MESSAGE =
'Logo must be a valid PNG, JPEG, GIF, or WebP image.';
private async handleCategoryIcon(req: any): Promise<{ id: string; publicUrl: string; width: number; height: number; mimeType: string; storedPath: string; size: number; originalFilename: string }> {
const file = req.file as any;
if (!file) throw new BadRequestException('No file uploaded under field "file"');
@@ -120,12 +135,22 @@ export class UploadsController {
};
}
private handleLogoUpload(req: any): { publicUrl: string; originalFilename: string } {
private async handleLogoUpload(req: any): Promise<{ publicUrl: string; originalFilename: string }> {
const file = req.file as any;
if (!file) throw new BadRequestException('No file uploaded under field "file"');
if (file.size > this.globalLimit) {
throw new BadRequestException(`File exceeds UPLOAD_SIZE_LIMIT (${file.size} > ${this.globalLimit})`);
}
let metadata: { format?: keyof FormatEnum; width?: number; height?: number };
try {
metadata = await sharp(file.buffer).metadata();
} catch {
throw new BadRequestException(UploadsController.LOGO_ERROR_MESSAGE);
}
const fmt = metadata.format;
if (!fmt || !UploadsController.ALLOWED_LOGO_FORMATS.has(fmt)) {
throw new BadRequestException(UploadsController.LOGO_ERROR_MESSAGE);
}
const safeName = safeFilename(file.originalname || 'logo');
// Files live at the top level of UPLOAD_DIR (served directly at /uploads/...).
const finalPath = path.join(this.uploadDir, safeName);