Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,8 @@ JWT_EXPIRES_IN=7d

# Development Settings
NODE_ENV=development

# File Upload Configuration
FILE_UPLOAD_DIR=./uploads
MAX_FILE_SIZE=10485760
ALLOWED_MIME_TYPES=image/jpeg,image/png,image/gif,image/webp,application/pdf,text/plain
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,8 @@ JWT_EXPIRES_IN=7d

# Development Settings
NODE_ENV=development

# File Upload Configuration
FILE_UPLOAD_DIR=./uploads
MAX_FILE_SIZE=10485760
ALLOWED_MIME_TYPES=image/jpeg,image/png,image/gif,image/webp,application/pdf,text/plain
5 changes: 4 additions & 1 deletion backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,23 @@ import { MonitoringModule } from './modules/monitoring/monitoring.module';
import { DatabaseModule } from './modules/database/database.module';
import { LoggingModule } from './modules/logging/logging.module';
import { AuthModule } from './modules/auth/auth.module';
import { FileUploadModule } from './modules/file-upload/file-upload.module';
import { ConditionalAuthGuard } from './modules/auth/guards/conditional-auth.guard';
import { SwaggerModule } from './modules/swagger/swagger.module';
import { authConfig } from './config/auth.config';
import { fileUploadConfig } from './config/file-upload.config';

@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
load: [authConfig],
load: [authConfig, fileUploadConfig],
}),
LoggingModule,
DatabaseModule,
MonitoringModule,
AuthModule,
FileUploadModule,
SwaggerModule,
],
providers: [
Expand Down
21 changes: 21 additions & 0 deletions backend/src/config/file-upload.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { registerAs } from '@nestjs/config';
import { join } from 'path';

export const fileUploadConfig = registerAs('fileUpload', () => ({
uploadDir: process.env.FILE_UPLOAD_DIR || join(process.cwd(), 'uploads'),
maxFileSize: parseInt(process.env.MAX_FILE_SIZE, 10) || 10 * 1024 * 1024,
allowedMimeTypes: process.env.ALLOWED_MIME_TYPES
? process.env.ALLOWED_MIME_TYPES.split(',')
: [
'image/jpeg',
'image/png',
'image/gif',
'image/webp',
'application/pdf',
'text/plain',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
],
}));
19 changes: 19 additions & 0 deletions backend/src/migrations/20240320000000_create_files_table.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { Knex } from 'knex';

exports.up = async function(knex: Knex): Promise<void> {
await knex.schema.createTable('files', (table) => {
table.string('id').primary();
table.string('user_id').notNullable().references('id').inTable('users').onDelete('CASCADE');
table.string('original_name').notNullable();
table.string('file_name').notNullable();
table.string('file_path').notNullable();
table.bigInteger('file_size').notNullable();
table.string('mime_type').notNullable();
table.timestamp('created_at').defaultTo(knex.fn.now());
table.timestamp('updated_at').defaultTo(knex.fn.now());
});
};

exports.down = async function(knex: Knex): Promise<void> {
await knex.schema.dropTable('files');
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import {
Controller,
Post,
Get,
Delete,
Param,
UseInterceptors,
UploadedFile,
Request,
Res,
BadRequestException,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { Response } from 'express';
import { FileUploadService } from '../services/file-upload.service';
import { FileResponseDto } from '../dto/file-response.dto';

@Controller('api/files')
export class FileUploadController {
constructor(private readonly fileUploadService: FileUploadService) {}

@Post('upload')
@UseInterceptors(FileInterceptor('file'))
async uploadFile(
@UploadedFile() file: Express.Multer.File,
@Request() req: any,
): Promise<FileResponseDto> {
if (!file) {
throw new BadRequestException('No file uploaded');
}

const userId = req.user?.id;
if (!userId) {
throw new BadRequestException('User not authenticated');
}

return this.fileUploadService.uploadFile(
userId,
file.originalname,
file.mimetype,
file.buffer,
);
}

@Get()
async getFiles(@Request() req: any): Promise<FileResponseDto[]> {
const userId = req.user?.id;
if (!userId) {
throw new BadRequestException('User not authenticated');
}

return this.fileUploadService.getFilesByUser(userId);
}

@Get(':id')
async downloadFile(
@Param('id') id: string,
@Request() req: any,
@Res() res: Response,
): Promise<void> {
const userId = req.user?.id;
if (!userId) {
throw new BadRequestException('User not authenticated');
}

const { file, stream } = await this.fileUploadService.getFileById(id, userId);

res.set({
'Content-Type': file.mimeType,
'Content-Disposition': `attachment; filename="${encodeURIComponent(file.originalName)}"`,
'Content-Length': file.fileSize,
});

stream.pipe(res);
}

@Delete(':id')
async deleteFile(
@Param('id') id: string,
@Request() req: any,
): Promise<{ message: string }> {
const userId = req.user?.id;
if (!userId) {
throw new BadRequestException('User not authenticated');
}

await this.fileUploadService.deleteFile(id, userId);
return { message: 'File deleted successfully' };
}
}
10 changes: 10 additions & 0 deletions backend/src/modules/file-upload/dto/file-response.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export class FileResponseDto {
id: string;
userId: string;
originalName: string;
fileName: string;
fileSize: number;
mimeType: string;
createdAt: Date;
updatedAt: Date;
}
11 changes: 11 additions & 0 deletions backend/src/modules/file-upload/entities/file.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export class File {
id: string;
userId: string;
originalName: string;
fileName: string;
filePath: string;
fileSize: number;
mimeType: string;
createdAt: Date;
updatedAt: Date;
}
21 changes: 21 additions & 0 deletions backend/src/modules/file-upload/file-upload.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { MulterModule } from '@nestjs/platform-express';
import { memoryStorage } from 'multer';
import { FileUploadController } from './controllers/file-upload.controller';
import { FileUploadService } from './services/file-upload.service';
import { FileRepository } from './repositories/file.repository';
import { fileUploadConfig } from '../../config/file-upload.config';

@Module({
imports: [
ConfigModule.forFeature(fileUploadConfig),
MulterModule.register({
storage: memoryStorage(),
}),
],
controllers: [FileUploadController],
providers: [FileUploadService, FileRepository],
exports: [FileUploadService],
})
export class FileUploadModule {}
72 changes: 72 additions & 0 deletions backend/src/modules/file-upload/repositories/file.repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { Injectable } from '@nestjs/common';
import { ulid } from 'ulid';
import { DatabaseService } from '../../database/database.service';
import { File } from '../entities/file.entity';

@Injectable()
export class FileRepository {
constructor(private readonly databaseService: DatabaseService) {}

private get knex() {
return this.databaseService.knex;
}

async create(fileData: Omit<File, 'id' | 'createdAt' | 'updatedAt'>): Promise<File> {
const id = ulid();
const now = new Date();

const [createdFile] = await this.knex('files')
.insert({
id,
user_id: fileData.userId,
original_name: fileData.originalName,
file_name: fileData.fileName,
file_path: fileData.filePath,
file_size: fileData.fileSize,
mime_type: fileData.mimeType,
created_at: now,
updated_at: now,
})
.returning('*');

return this.mapToEntity(createdFile);
}

async findById(id: string): Promise<File | null> {
const file = await this.knex('files')
.where('id', id)
.first();

return file ? this.mapToEntity(file) : null;
}

async findByUserId(userId: string): Promise<File[]> {
const files = await this.knex('files')
.where('user_id', userId)
.orderBy('created_at', 'desc');

return files.map(this.mapToEntity);
}

async delete(id: string): Promise<boolean> {
const deletedCount = await this.knex('files')
.where('id', id)
.delete();

return deletedCount > 0;
}

private mapToEntity(row: any): File {
return {
id: row.id,
userId: row.user_id,
originalName: row.original_name,
fileName: row.file_name,
filePath: row.file_path,
fileSize: Number(row.file_size),
mimeType: row.mime_type,
createdAt: row.created_at,
updatedAt: row.updated_at,
};
}
}
Loading