diff --git a/backend/app.js b/backend/app.js index 291d596..bb2d984 100644 --- a/backend/app.js +++ b/backend/app.js @@ -104,7 +104,7 @@ app.use((req, res, next) => { return next(); } - express.json({ limit: '10mb' })(req, res, next); + express.json({ limit: `${config.fileUploadLimitMB}mb` })(req, res, next); }); app.use((req, res, next) => { @@ -116,7 +116,10 @@ app.use((req, res, next) => { return next(); } - express.urlencoded({ extended: true, limit: '10mb' })(req, res, next); + express.urlencoded({ + extended: true, + limit: `${config.fileUploadLimitMB}mb`, + })(req, res, next); }); // CalDAV routes (registered after conditional body parsers) diff --git a/backend/config/config.js b/backend/config/config.js index 54e5695..5f662f1 100644 --- a/backend/config/config.js +++ b/backend/config/config.js @@ -103,6 +103,11 @@ const config = { uploadPath: process.env.TUDUDI_UPLOAD_PATH || path.join(projectRootPath, 'uploads'), + // File upload limit in MB (default 10MB) + fileUploadLimitMB: process.env.FILE_UPLOAD_LIMIT_MB + ? parseInt(process.env.FILE_UPLOAD_LIMIT_MB, 10) + : 10, + // API Documentation (Swagger) swagger: { enabled: process.env.SWAGGER_ENABLED !== 'false', diff --git a/backend/modules/auth/controller.js b/backend/modules/auth/controller.js index 5644f91..7ac0093 100644 --- a/backend/modules/auth/controller.js +++ b/backend/modules/auth/controller.js @@ -4,12 +4,20 @@ const authService = require('./service'); const { logError } = require('../../services/logService'); const { generateToken } = require('../../middleware/csrf'); const { isPasswordAuthEnabled } = require('../../config/authConfig'); +const { getConfig } = require('../../config/config'); const authController = { getVersion(req, res) { res.json(authService.getVersion()); }, + getPublicConfig(req, res) { + const config = getConfig(); + res.json({ + fileUploadLimitMB: config.fileUploadLimitMB, + }); + }, + async getRegistrationStatus(req, res, next) { try { const result = await authService.getRegistrationStatus(); diff --git a/backend/modules/auth/routes.js b/backend/modules/auth/routes.js index 11c86c1..672da41 100644 --- a/backend/modules/auth/routes.js +++ b/backend/modules/auth/routes.js @@ -7,6 +7,7 @@ const { authLimiter, apiLimiter } = require('../../middleware/rateLimiter'); const { csrfMiddleware } = require('../../middleware/csrf'); router.get('/version', authController.getVersion); +router.get('/config', authController.getPublicConfig); router.get('/registration-status', authController.getRegistrationStatus); router.get( '/password-auth-status', diff --git a/backend/modules/projects/routes.js b/backend/modules/projects/routes.js index 792ca8a..ffb8f26 100644 --- a/backend/modules/projects/routes.js +++ b/backend/modules/projects/routes.js @@ -29,7 +29,7 @@ const storage = multer.diskStorage({ const upload = multer({ storage: storage, limits: { - fileSize: 10 * 1024 * 1024, // 10MB limit + fileSize: config.fileUploadLimitMB * 1024 * 1024, }, fileFilter: function (req, file, cb) { const allowedTypes = /jpeg|jpg|png|gif|webp/; diff --git a/backend/modules/tasks/attachments.js b/backend/modules/tasks/attachments.js index ddedcd1..46dce50 100644 --- a/backend/modules/tasks/attachments.js +++ b/backend/modules/tasks/attachments.js @@ -49,7 +49,7 @@ const storage = multer.diskStorage({ const upload = multer({ storage: storage, limits: { - fileSize: 10 * 1024 * 1024, // 10MB limit + fileSize: config.fileUploadLimitMB * 1024 * 1024, }, fileFilter: function (req, file, cb) { if (validateFileType(file.mimetype)) { diff --git a/docs/00-tasks-behavior.md b/docs/00-tasks-behavior.md index e766e13..289b521 100644 --- a/docs/00-tasks-behavior.md +++ b/docs/00-tasks-behavior.md @@ -157,7 +157,7 @@ This document explains how tasks work in tududi from a user behavior perspective 22. **Tasks can have file attachments:** - Multiple files can be attached to a single task - - File size limit: 10MB per file + - File size limit: 10MB per file (configurable via `FILE_UPLOAD_LIMIT_MB` environment variable) - Stored in `/uploads/tasks/` directory 23. **Allowed file types:** diff --git a/docs/development-workflow.md b/docs/development-workflow.md index 0ae4f4c..1e5850f 100644 --- a/docs/development-workflow.md +++ b/docs/development-workflow.md @@ -115,6 +115,7 @@ FRONTEND_URL=http://localhost:8080 BACKEND_URL=http://localhost:3002 PORT=3002 HOST=0.0.0.0 +FILE_UPLOAD_LIMIT_MB=10 # Optional - Email ENABLE_EMAIL=false diff --git a/frontend/components/Task/TaskDetails/TaskAttachmentsCard.tsx b/frontend/components/Task/TaskDetails/TaskAttachmentsCard.tsx index 3dbc1c7..b15d44f 100644 --- a/frontend/components/Task/TaskDetails/TaskAttachmentsCard.tsx +++ b/frontend/components/Task/TaskDetails/TaskAttachmentsCard.tsx @@ -73,7 +73,7 @@ const TaskAttachmentsCard: React.FC = ({ if (!file) return; // Validate file - const validation = validateFile(file); + const validation = await validateFile(file); if (!validation.valid) { showErrorToast(validation.error || 'Invalid file'); if (fileInputRef.current) { diff --git a/frontend/components/Task/TaskForm/TaskAttachmentsSection.tsx b/frontend/components/Task/TaskForm/TaskAttachmentsSection.tsx index 220a2a2..76d5e25 100644 --- a/frontend/components/Task/TaskForm/TaskAttachmentsSection.tsx +++ b/frontend/components/Task/TaskForm/TaskAttachmentsSection.tsx @@ -37,7 +37,7 @@ const TaskAttachmentsSection: React.FC = ({ if (!file) return; // Validate file - const validation = validateFile(file); + const validation = await validateFile(file); if (!validation.valid) { showErrorToast(validation.error || 'Invalid file'); if (fileInputRef.current) { diff --git a/frontend/utils/attachmentsService.ts b/frontend/utils/attachmentsService.ts index 76c9fb6..0e5f134 100644 --- a/frontend/utils/attachmentsService.ts +++ b/frontend/utils/attachmentsService.ts @@ -1,6 +1,7 @@ import { Attachment, AttachmentType } from '../entities/Attachment'; import { getApiPath } from '../config/paths'; import { getCsrfToken } from './csrfService'; +import { getServerConfig } from './configService'; /** * Upload a file attachment to a task @@ -146,13 +147,15 @@ export function formatFileSize(bytes: number): string { /** * Validate file before upload */ -export function validateFile(file: File): { valid: boolean; error?: string } { - // Check file size (10MB max) - const maxSize = 10 * 1024 * 1024; +export async function validateFile( + file: File +): Promise<{ valid: boolean; error?: string }> { + const config = await getServerConfig(); + const maxSize = config.fileUploadLimitMB * 1024 * 1024; if (file.size > maxSize) { return { valid: false, - error: 'File size exceeds 10MB limit', + error: `File size exceeds ${config.fileUploadLimitMB}MB limit`, }; } diff --git a/frontend/utils/configService.ts b/frontend/utils/configService.ts new file mode 100644 index 0000000..d034db0 --- /dev/null +++ b/frontend/utils/configService.ts @@ -0,0 +1,29 @@ +import { getApiPath } from '../config/paths'; + +interface ServerConfig { + fileUploadLimitMB: number; +} + +let cachedConfig: ServerConfig | null = null; + +export async function getServerConfig(): Promise { + if (cachedConfig) { + return cachedConfig; + } + + const response = await fetch(getApiPath('config'), { + method: 'GET', + credentials: 'include', + }); + + if (!response.ok) { + throw new Error('Failed to fetch server configuration'); + } + + cachedConfig = await response.json(); + return cachedConfig; +} + +export function getFileUploadLimitMB(): number { + return cachedConfig?.fileUploadLimitMB || 10; +}