DocumentsModule (Document: entityType/entityId, Dateiname, MIME-Typ, Groesse, Speicherpfad, Hochgeladen-von) fuer Bienenstand/Volk/Koenigin/ Durchsicht/Honigcharge. Upload als multipart/form-data (Multer, 10 MB Limit, MIME-Allowlist jpeg/png/webp/pdf), Speicherung unter DOCUMENTS_STORAGE_PATH (austauschbar). ARCHITECTURE.md Annahme 7 dokumentiert die Multi-Replica-Einschraenkung ohne geteiltes Volume.
112 lines
3.1 KiB
TypeScript
112 lines
3.1 KiB
TypeScript
import {
|
|
BadRequestException,
|
|
Body,
|
|
Controller,
|
|
Delete,
|
|
Get,
|
|
HttpCode,
|
|
Param,
|
|
Post,
|
|
Query,
|
|
Res,
|
|
UploadedFile,
|
|
UseInterceptors,
|
|
} from '@nestjs/common';
|
|
import { FileInterceptor } from '@nestjs/platform-express';
|
|
import { ApiBearerAuth, ApiConsumes, ApiTags } from '@nestjs/swagger';
|
|
import type { Response } from 'express';
|
|
import { DocumentEntityType, Role } from '../../generated/prisma';
|
|
import { Roles } from '../auth/decorators/roles.decorator';
|
|
import { CurrentUser } from '../auth/decorators/current-user.decorator';
|
|
import type { AuthenticatedUser } from '../auth/types/authenticated-user';
|
|
import { isAllowedDocumentMimeType } from './document-mime-types';
|
|
import { CreateDocumentDto } from './dto/create-document.dto';
|
|
import { DocumentsService } from './documents.service';
|
|
|
|
const MAX_FILE_SIZE_BYTES = 10 * 1024 * 1024;
|
|
|
|
function sanitizeFilenameHeader(fileName: string): string {
|
|
return fileName.replace(/[\r\n"]/g, '_');
|
|
}
|
|
|
|
@ApiTags('documents')
|
|
@ApiBearerAuth()
|
|
@Controller('documents')
|
|
export class DocumentsController {
|
|
constructor(private readonly documentsService: DocumentsService) {}
|
|
|
|
@Get()
|
|
findAll(
|
|
@Query('entityType') entityType: string,
|
|
@Query('entityId') entityId: string,
|
|
) {
|
|
if (!entityType || !entityId) {
|
|
throw new BadRequestException('entityType and entityId are required');
|
|
}
|
|
if (
|
|
!Object.values(DocumentEntityType).includes(
|
|
entityType as DocumentEntityType,
|
|
)
|
|
) {
|
|
throw new BadRequestException('Invalid entityType');
|
|
}
|
|
return this.documentsService.findAll(
|
|
entityType as DocumentEntityType,
|
|
entityId,
|
|
);
|
|
}
|
|
|
|
@Roles(Role.ADMIN, Role.BEEKEEPER, Role.STAFF)
|
|
@Post()
|
|
@ApiConsumes('multipart/form-data')
|
|
@UseInterceptors(
|
|
FileInterceptor('file', {
|
|
limits: { fileSize: MAX_FILE_SIZE_BYTES },
|
|
fileFilter: (_req, file, callback) => {
|
|
if (!isAllowedDocumentMimeType(file.mimetype)) {
|
|
callback(
|
|
new BadRequestException('Nicht unterstützter Dateityp'),
|
|
false,
|
|
);
|
|
return;
|
|
}
|
|
callback(null, true);
|
|
},
|
|
}),
|
|
)
|
|
create(
|
|
@UploadedFile() file: Express.Multer.File | undefined,
|
|
@Body() dto: CreateDocumentDto,
|
|
@CurrentUser() actor: AuthenticatedUser,
|
|
) {
|
|
if (!file) {
|
|
throw new BadRequestException('No file uploaded');
|
|
}
|
|
return this.documentsService.create(
|
|
file,
|
|
dto.entityType,
|
|
dto.entityId,
|
|
actor.id,
|
|
);
|
|
}
|
|
|
|
@Get(':id/download')
|
|
async download(@Param('id') id: string, @Res() res: Response): Promise<void> {
|
|
const { buffer, fileName, mimeType } =
|
|
await this.documentsService.readFile(id);
|
|
res.setHeader('Content-Type', mimeType);
|
|
res.setHeader(
|
|
'Content-Disposition',
|
|
`attachment; filename="${sanitizeFilenameHeader(fileName)}"`,
|
|
);
|
|
res.send(buffer);
|
|
}
|
|
|
|
@Roles(Role.ADMIN, Role.BEEKEEPER, Role.STAFF)
|
|
@Delete(':id')
|
|
@HttpCode(204)
|
|
delete(@Param('id') id: string, @CurrentUser() actor: AuthenticatedUser) {
|
|
return this.documentsService.delete(id, actor.id);
|
|
}
|
|
}
|