AI Implementation feature(861): Admin Area Challenges: List, Import/Export and Add/Edit Modal (#40)
This commit was merged in pull request #40.
This commit is contained in:
@@ -9,6 +9,15 @@ import { AdminGuard } from '../../common/guards/admin.guard';
|
||||
import { Roles } from '../../common/decorators/roles.decorator';
|
||||
import { parseUploadSizeLimit, safeFilename } from '../../common/utils/upload';
|
||||
|
||||
/**
|
||||
* Multipart upload endpoints for site logos and category icons.
|
||||
*
|
||||
* Challenge attachments use the staged lifecycle exposed by
|
||||
* `AdminChallengesController` (`POST /api/v1/admin/challenges/files/stage`),
|
||||
* which guarantees the file only becomes "real" once the admin saves the
|
||||
* challenge. The previous direct-to-final challenge-file route was removed
|
||||
* because it bypassed staging and could overwrite files by name.
|
||||
*/
|
||||
@ApiTags('uploads')
|
||||
@ApiBearerAuth()
|
||||
@Controller('api/v1/uploads')
|
||||
@@ -32,15 +41,6 @@ export class UploadsController {
|
||||
return this.handleCategoryIcon(req);
|
||||
}
|
||||
|
||||
@Post('challenge-file')
|
||||
@HttpCode(HttpStatus.CREATED)
|
||||
@ApiOperation({ summary: 'Upload a challenge attachment (admin only)' })
|
||||
@ApiConsumes('multipart/form-data')
|
||||
@UseInterceptors(FileInterceptor('file'))
|
||||
async uploadChallengeFile(@Req() req: any) {
|
||||
return this.handleGenericUpload(req, 'challenges');
|
||||
}
|
||||
|
||||
@Post('logo')
|
||||
@HttpCode(HttpStatus.CREATED)
|
||||
@ApiOperation({ summary: 'Upload the site logo (admin only)' })
|
||||
@@ -78,9 +78,6 @@ export class UploadsController {
|
||||
const finalDir = path.join(this.uploadDir, 'icons');
|
||||
if (!fs.existsSync(finalDir)) fs.mkdirSync(finalDir, { recursive: true });
|
||||
|
||||
// When categoryId is provided we normalize to 128x128 and store at a
|
||||
// deterministic URL keyed by id. When it is omitted (legacy / pre-existing
|
||||
// tests) we fall back to a safe version of the original filename.
|
||||
const hasId = !!categoryId;
|
||||
const fileName = hasId ? `${categoryId}.png` : safeFilename(file.originalname || 'file.png');
|
||||
const finalPath = path.join(finalDir, fileName);
|
||||
@@ -110,28 +107,6 @@ export class UploadsController {
|
||||
};
|
||||
}
|
||||
|
||||
private handleGenericUpload(req: any, subdir: 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})`);
|
||||
}
|
||||
const safeName = (file.originalname || 'file').replace(/[^a-zA-Z0-9._-]/g, '_');
|
||||
const finalDir = path.join(this.uploadDir, subdir);
|
||||
if (!fs.existsSync(finalDir)) fs.mkdirSync(finalDir, { recursive: true });
|
||||
const finalPath = path.join(finalDir, safeName);
|
||||
fs.writeFileSync(finalPath, file.buffer);
|
||||
const publicUrl = `/uploads/${subdir}/${safeName}`;
|
||||
return {
|
||||
id: safeName,
|
||||
originalFilename: file.originalname,
|
||||
storedPath: finalPath,
|
||||
publicUrl,
|
||||
size: file.size,
|
||||
mimeType: file.mimetype,
|
||||
};
|
||||
}
|
||||
|
||||
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"');
|
||||
@@ -149,7 +124,6 @@ export class UploadsController {
|
||||
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);
|
||||
fs.writeFileSync(finalPath, file.buffer);
|
||||
return {
|
||||
@@ -157,4 +131,4 @@ export class UploadsController {
|
||||
originalFilename: file.originalname || safeName,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user