From 2df1b28962614449dfe49524ea852be9c720f99c Mon Sep 17 00:00:00 2001 From: Dinawo Date: Sat, 25 Oct 2025 23:55:51 +0200 Subject: [PATCH] Update v1.2.0-beta - Dynamic context menu & permissions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ✹ New Features: - Dynamic permission-based context menus for files and folders - Support for collaborative folder access control - Upload to specific folders including shared folders - Changelog modal for version updates - Improved dark mode synchronization 🐛 Bug Fixes: - Fixed context menu displaying incorrect options - Fixed CSS !important override preventing dynamic menu behavior - Fixed folder collaboration permission checks - Fixed breadcrumb navigation with empty segments - Fixed "Premature close" error loop in attachments - Fixed missing user variable in admin routes - Fixed avatar loading COEP policy issues 🔒 Security: - Added security middleware (CSRF, rate limiting, input validation) - Fixed collaboration folder access validation - Improved shared folder permission handling 🎹 UI/UX Improvements: - Removed Actions column from folder view - Context menu now properly hides/shows based on permissions - Better visual feedback for collaborative folders - Improved upload flow with inline modals đŸ§č Code Quality: - Added collaboration data to folder routes - Refactored context menu logic for better maintainability - Added debug logging for troubleshooting - Improved file upload handling with chunking support --- .claude/settings.local.json | 8 + CLAUDE.md | 234 +++++ Middlewares/csrfProtectionMiddleware.js | 170 ++++ Middlewares/inputValidationMiddleware.js | 202 +++++ Middlewares/rateLimitMiddleware.js | 194 ++++ Middlewares/securityHeadersMiddleware.js | 76 ++ docker-compose.yml | 2 +- package-lock.json | 725 ++++++++------- package.json | 2 +- public/css/admin.styles.css | 457 ++++++++++ public/css/dashboard.styles.css | 484 ++++++---- public/css/dropdown-fixes.css | 12 + public/js/dashboard.js | 321 ++++++- public/js/folder.js | 43 +- public/js/paramadminsettingsetup.script.js | 7 + routes/Dpanel/API/DeleteFolfder.js | 3 +- routes/Dpanel/API/SharedFolders.js | 36 + routes/Dpanel/API/Upload.js | 37 +- routes/Dpanel/Admin/Privacy-Security.js | 2 +- routes/Dpanel/Admin/Stats-Logs.js | 2 +- routes/Dpanel/Admin/User.js | 2 +- routes/Dpanel/Admin/index.js | 45 +- routes/Dpanel/Folder/index.js | 50 +- routes/attachments.js | 6 +- routes/routes.js | 2 + server.js | 28 +- views/dashboard.ejs | 538 ++++++++--- views/folder.ejs | 972 ++++++++++++++++---- views/paramAdmin.ejs | 797 +++++++++++++--- views/paramAdminPrivacy&Security.ejs | 570 ++++++++++-- views/paramAdminStats&Logs.ejs | 705 ++++++++++++--- views/paramAdminUser.ejs | 999 ++++++++++++++++----- views/profile.ejs | 6 - 33 files changed, 6275 insertions(+), 1462 deletions(-) create mode 100644 .claude/settings.local.json create mode 100644 CLAUDE.md create mode 100644 Middlewares/csrfProtectionMiddleware.js create mode 100644 Middlewares/inputValidationMiddleware.js create mode 100644 Middlewares/rateLimitMiddleware.js create mode 100644 Middlewares/securityHeadersMiddleware.js create mode 100644 public/css/admin.styles.css create mode 100644 public/css/dropdown-fixes.css create mode 100644 routes/Dpanel/API/SharedFolders.js diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..d2855f8 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,8 @@ +{ + "permissions": { + "allow": [ + "Bash(npm audit:*)" + ], + "deny": [] + } +} \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..f3f8aa9 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,234 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +CDN-APP-INSIDER is a self-hosted Content Delivery Network (CDN) application for secure file transfer and management. The application supports multiple authentication methods (Discord, LDAP/ActiveDirectory), file collaboration, and real-time WebSocket updates. + +**Version**: 1.2.0-beta +**Author**: Dinawo - Group Myaxrin Labs +**Main Contributor**: WaYy + +## Development Commands + +### Running the Application +```bash +npm start # Production mode +npm run nodemon # Development mode with auto-reload +``` + +### Installation +The application is typically installed via: +```bash +curl -s https://apollon.dinawo.fr/getcdn/install/latest | bash +``` + +After installation, access the dashboard at: `https://your-ip:3000/dpanel/dashboard` + +**Prerequisites**: A CDN-Access group must exist in your LDAP directory. + +## Architecture Overview + +### Core Components + +**server.js** - Application entry point that: +- Initializes Express app with session management and Passport authentication +- Configures authentication strategies based on `data/setup.json` (Discord, LDAP) +- Sets up WebSocket server for real-time updates +- Starts cron jobs for file cleanup and system reporting +- Protects sensitive JSON files in `/data/` directory +- Listens on port 3000 (configurable via `PORT` env variable) + +**routes/routes.js** - Central routing hub that imports and mounts all route modules with middleware chains + +### Authentication System + +The application uses Passport.js with multiple strategies: +- **Discord OAuth** (`models/Passport-Discord.js`) +- **LDAP/ActiveDirectory** (`models/Passport-ActiveDirectory.js`) +- **Google OAuth** (`models/Passport-Google.js`) + +Authentication strategies are conditionally loaded based on `data/setup.json` configuration. + +**authMiddleware.js** - Core authentication middleware that: +- Validates session authentication via `req.isAuthenticated()` +- Loads user data from `data/user.json` +- Attaches user object to `req.session.user`, `res.locals.user`, and `req.userData` +- Redirects unauthenticated users to `/auth/login` + +### Logging System (`config/logs.js`) + +Winston-based logging with: +- Daily rotating file logs (14-day retention, 20MB max size) +- Multiple specialized loggers: server, client, error, auth, suspicious, API, filesystem, database +- Configurable via `data/setup.json` with: + - `logs.enabled`: 'on' or 'off' + - `logs.level`: 'info', 'warn', 'error', etc. + - `logs.includeOnly`: Array of paths to exclusively log + - `logs.excludePaths`: Array of paths to exclude from logging + - `logs.levels`: Array of enabled log levels + +Logs are stored in `/logs/` directory with format: `log-YYYY-MM-DD.log` + +### Security & Ban System (`models/banModel.js`) + +Progressive ban system that: +- Tracks suspicious requests per IP in `data/banUser.json` +- Implements escalating ban levels: 10min, 30min, 60min, permanent +- Triggers after 5 suspicious requests within 60 seconds +- Excludes localhost and specific endpoints (ActiveDirectory, favicon) +- All suspicious activity is logged via `suspiciousLogger` + +**discordWebhookSuspisiousAlertMiddleware.js** - Sends Discord webhook alerts for suspicious API requests + +### WebSocket System (`models/websocketManager.js`) + +Real-time communication for: +- File collaboration status (who's viewing/editing) +- Broadcasting file updates to all connected clients +- Connection management keyed by userId and fileId +- Sends `fileStatus` messages with active users array + +Access via: `req.app.get('wsManager')` in routes + +### File Management + +**File Storage**: All uploaded files are stored in `/cdn-files/` directory + +**File Metadata**: Tracked in `data/file_info.json` with: +- File path, name, size, upload date +- Expiry date for automatic cleanup +- Owner information and permissions + +**File Cleanup Service** (`services/fileCleanupService.js`): +- Extends BaseService class +- Runs on cron schedule (default: hourly at `0 * * * *`) +- Removes expired files based on `expiryDate` +- Removes orphaned entries for missing files +- Updates `data/file_info.json` after cleanup + +**Report Service** (`services/reportService.js`): +- Generates system reports in `/report/` directory +- Runs on configurable cron schedule + +### Data Files + +Located in `/data/` directory (protected from direct HTTP access): +- **user.json** - User accounts and roles +- **setup.json** - Application configuration (auth providers, logging, etc.) +- **file_info.json** - File metadata registry +- **banUser.json** - IP ban tracking +- **collaboration.json** - File collaboration settings + +## Route Structure + +### Public Routes +- `/` - Landing page +- `/auth/login` - Login page +- `/auth/logout` - Logout handler +- `/auth/activedirectory` - AD/LDAP authentication callback +- `/auth/discord` - Discord OAuth callback +- `/attachments` - File serving endpoint +- `/build-metadata` - Build information + +### Dashboard Routes (`/dpanel/dashboard`) +- `/dpanel/dashboard` - Main dashboard (requires auth) +- `/dpanel/dashboard/folder` - Folder view +- `/dpanel/dashboard/profil` - User profile +- `/dpanel/upload` - File upload interface + +### Admin Routes (`/dpanel/dashboard/admin`) +Require admin role: +- `/dpanel/dashboard/admin` - Admin panel +- `/dpanel/dashboard/admin/users` - User management +- `/dpanel/dashboard/admin/settingsetup` - System settings +- `/dpanel/dashboard/admin/stats-logs` - Statistics and logs +- `/dpanel/dashboard/admin/Privacy-Security` - Security settings + +### API Routes (`/api/dpanel`) +All API routes use: +1. `discordWebhookSuspisiousAlertMiddleware` - Alerts on suspicious activity +2. `logApiRequest` - Logs API calls with timing + +Key endpoints: +- POST `/api/dpanel/upload` - File upload +- POST `/api/dpanel/dashboard/newfolder` - Create folder +- PUT `/api/dpanel/dashboard/rename` - Rename file +- PUT `/api/dpanel/folders/rename` - Rename folder +- DELETE `/api/dpanel/dashboard/delete` - Delete file +- DELETE `/api/dpanel/dashboard/deletefolder` - Delete folder +- POST `/api/dpanel/dashboard/movefile` - Move file +- POST `/api/dpanel/collaboration` - Manage file collaboration +- GET `/api/dpanel/users/search` - Search users +- GET/POST `/api/dpanel/sharedfolders` - Shared folder operations +- POST `/api/dpanel/generate-token` - Generate API token +- POST `/api/dpanel/revoke-token` - Revoke API token + +### API Documentation +Swagger UI available at: `/api/docs` + +## Middleware Chain + +Standard middleware chain for protected routes: +``` +authMiddleware → discordWebhookSuspisiousAlertMiddleware → logApiRequest → route handler +``` + +## Frontend + +- **View Engine**: EJS templates in `/views/` +- **Static Assets**: `/public/` directory +- **CSS**: Custom dashboard styles in `/public/css/dashboard.styles.css` +- **JavaScript**: Client-side logic in `/public/js/dashboard.js` +- **Styling**: TailwindCSS + DaisyUI components + +## Key Dependencies + +- **express** - Web framework +- **passport** - Authentication +- **socket.io** & **ws** - WebSocket support +- **winston** - Logging +- **node-cron** - Scheduled tasks +- **multer** & **express-fileupload** - File uploads +- **pg** & **mysql2** - Database support +- **bcrypt** - Password hashing +- **jsonwebtoken** - JWT tokens + +## Important Notes + +### Security Considerations +- All `/data/*.json` files are protected from direct HTTP access +- Session secrets are generated using `crypto.randomBytes(64)` +- Cookies are secure in production (`NODE_ENV=production`) +- Rate limiting via `express-rate-limit` +- Progressive ban system for suspicious activity + +### File Paths +- Always use `path.join(__dirname, ...)` for file paths +- Normalize paths with `path.normalize()` and replace backslashes +- File paths in metadata use forward slashes + +### Session Management +- User data is stored in both session and attached to `req.userData` +- Session maxAge: 24 hours +- Sessions persist across server restarts via session storage + +### Error Handling +- Global error handlers catch uncaught exceptions and unhandled rejections +- Errors are logged via `ErrorLogger` from config/logs +- API errors return JSON with `{ error, message }` structure +- HTML requests receive rendered error pages + +### WebSocket Events +- `join` - User joins file view (params: userId, fileId) +- `leave` - User leaves file view (params: fileId) +- `fileStatus` - Broadcast of active users on a file + +## Testing & Debugging + +- Winston logs are colorized in console for easier debugging +- Request logging includes IP, User-Agent, method, URL, and timing +- API requests log response status and duration +- Suspicious activity is highlighted with orange prefix +- Error logs include full stack traces diff --git a/Middlewares/csrfProtectionMiddleware.js b/Middlewares/csrfProtectionMiddleware.js new file mode 100644 index 0000000..1b2a540 --- /dev/null +++ b/Middlewares/csrfProtectionMiddleware.js @@ -0,0 +1,170 @@ +/** + * Middleware de protection CSRF (Cross-Site Request Forgery) + * GĂ©nĂšre et valide des tokens CSRF pour les requĂȘtes sensibles + */ + +const crypto = require('crypto'); +const { logger } = require('../config/logs'); + +// Stockage en mĂ©moire des tokens CSRF (en production, utiliser Redis ou une base de donnĂ©es) +const csrfTokens = new Map(); + +// DurĂ©e de vie d'un token CSRF (30 minutes) +const TOKEN_LIFETIME = 30 * 60 * 1000; + +/** + * GĂ©nĂšre un token CSRF pour une session + */ +const generateCsrfToken = (sessionId) => { + const token = crypto.randomBytes(32).toString('hex'); + const expiresAt = Date.now() + TOKEN_LIFETIME; + + csrfTokens.set(sessionId, { + token, + expiresAt + }); + + // Nettoyer les tokens expirĂ©s toutes les 5 minutes + setInterval(() => { + const now = Date.now(); + for (const [id, data] of csrfTokens.entries()) { + if (data.expiresAt < now) { + csrfTokens.delete(id); + } + } + }, 5 * 60 * 1000); + + return token; +}; + +/** + * Valide un token CSRF + */ +const validateCsrfToken = (sessionId, token) => { + const storedData = csrfTokens.get(sessionId); + + if (!storedData) { + return false; + } + + if (storedData.expiresAt < Date.now()) { + csrfTokens.delete(sessionId); + return false; + } + + return storedData.token === token; +}; + +/** + * Middleware pour ajouter le token CSRF Ă  la requĂȘte + */ +const csrfTokenMiddleware = (req, res, next) => { + // GĂ©nĂ©rer un ID de session si nĂ©cessaire + if (!req.session) { + return next(); + } + + const sessionId = req.session.id || req.sessionID; + + // GĂ©nĂ©rer ou rĂ©cupĂ©rer le token CSRF + let csrfToken = null; + const storedData = csrfTokens.get(sessionId); + + if (storedData && storedData.expiresAt > Date.now()) { + csrfToken = storedData.token; + } else { + csrfToken = generateCsrfToken(sessionId); + } + + // Ajouter le token aux locals pour l'utiliser dans les vues + res.locals.csrfToken = csrfToken; + + // Ajouter une mĂ©thode helper + req.csrfToken = () => csrfToken; + + next(); +}; + +/** + * Middleware de validation CSRF pour les mĂ©thodes POST, PUT, DELETE + */ +const csrfProtectionMiddleware = (req, res, next) => { + // Ignorer les requĂȘtes GET, HEAD, OPTIONS + if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) { + return next(); + } + + // Ignorer certaines routes (API publiques, webhooks, etc.) + const exemptPaths = [ + '/auth/activedirectory/callback', + '/auth/discord/callback', + '/api/webhook' + ]; + + if (exemptPaths.some(path => req.path.startsWith(path))) { + return next(); + } + + // VĂ©rifier si la session existe + if (!req.session) { + logger.warn(`CSRF protection: No session found for ${req.method} ${req.path} from ${req.ip}`); + return res.status(403).json({ + error: 'Session invalide', + code: 'NO_SESSION' + }); + } + + const sessionId = req.session.id || req.sessionID; + + // RĂ©cupĂ©rer le token CSRF de la requĂȘte + const token = req.body._csrf || + req.query._csrf || + req.headers['x-csrf-token'] || + req.headers['csrf-token']; + + if (!token) { + logger.warn(`CSRF protection: No token provided for ${req.method} ${req.path} from ${req.ip}`); + return res.status(403).json({ + error: 'Token CSRF manquant', + code: 'CSRF_TOKEN_MISSING' + }); + } + + // Valider le token + if (!validateCsrfToken(sessionId, token)) { + logger.warn(`CSRF protection: Invalid token for ${req.method} ${req.path} from ${req.ip}`); + return res.status(403).json({ + error: 'Token CSRF invalide ou expirĂ©', + code: 'CSRF_TOKEN_INVALID' + }); + } + + next(); +}; + +/** + * Route pour obtenir un nouveau token CSRF + */ +const getCsrfToken = (req, res) => { + const sessionId = req.session?.id || req.sessionID; + + if (!sessionId) { + return res.status(400).json({ + error: 'Session invalide' + }); + } + + const token = generateCsrfToken(sessionId); + + res.json({ + csrfToken: token + }); +}; + +module.exports = { + csrfTokenMiddleware, + csrfProtectionMiddleware, + getCsrfToken, + generateCsrfToken, + validateCsrfToken +}; diff --git a/Middlewares/inputValidationMiddleware.js b/Middlewares/inputValidationMiddleware.js new file mode 100644 index 0000000..5571996 --- /dev/null +++ b/Middlewares/inputValidationMiddleware.js @@ -0,0 +1,202 @@ +/** + * Middleware de validation et d'assainissement des entrĂ©es utilisateur + * ProtĂšge contre les injections SQL, XSS, et autres attaques + */ + +const { logger } = require('../config/logs'); + +/** + * Nettoie une chaĂźne de caractĂšres pour prĂ©venir les injections + */ +const sanitizeString = (str) => { + if (typeof str !== 'string') return str; + + // Supprime les caractĂšres null bytes + str = str.replace(/\0/g, ''); + + // Encode les caractĂšres spĂ©ciaux HTML + str = str + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') + .replace(/\//g, '/'); + + return str.trim(); +}; + +/** + * Valide un nom de fichier/dossier + */ +const validateFileName = (filename) => { + if (!filename || typeof filename !== 'string') { + return { valid: false, error: 'Nom de fichier invalide' }; + } + + // Limite de longueur + if (filename.length > 255) { + return { valid: false, error: 'Nom de fichier trop long (max 255 caractĂšres)' }; + } + + // CaractĂšres interdits dans les noms de fichiers Windows/Linux + const forbiddenChars = /[<>:"\/\\|?*\x00-\x1f]/g; + if (forbiddenChars.test(filename)) { + return { valid: false, error: 'CaractĂšres interdits dans le nom de fichier' }; + } + + // Noms rĂ©servĂ©s Windows + const reservedNames = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i; + if (reservedNames.test(filename.split('.')[0])) { + return { valid: false, error: 'Nom de fichier rĂ©servĂ©' }; + } + + // VĂ©rifier les tentatives de traversĂ©e de rĂ©pertoire + if (filename.includes('..') || filename.includes('./') || filename.includes('.\\')) { + return { valid: false, error: 'Tentative de traversĂ©e de rĂ©pertoire dĂ©tectĂ©e' }; + } + + return { valid: true }; +}; + +/** + * Valide un chemin de dossier + */ +const validatePath = (path) => { + if (!path || typeof path !== 'string') { + return { valid: false, error: 'Chemin invalide' }; + } + + // VĂ©rifier les tentatives de traversĂ©e de rĂ©pertoire + if (path.includes('..') || path.includes('~')) { + return { valid: false, error: 'Tentative de traversĂ©e de rĂ©pertoire dĂ©tectĂ©e' }; + } + + // VĂ©rifier les chemins absolus non autorisĂ©s + if (path.startsWith('/') || /^[A-Za-z]:/.test(path)) { + return { valid: false, error: 'Chemin absolu non autorisĂ©' }; + } + + return { valid: true }; +}; + +/** + * Valide un ID utilisateur + */ +const validateUserId = (userId) => { + if (!userId) { + return { valid: false, error: 'ID utilisateur manquant' }; + } + + // Accepte les formats Discord (snowflake) ou alphanumĂ©riques + if (!/^[a-zA-Z0-9_-]{1,100}$/.test(userId)) { + return { valid: false, error: 'Format d\'ID utilisateur invalide' }; + } + + return { valid: true }; +}; + +/** + * Valide une adresse email + */ +const validateEmail = (email) => { + if (!email || typeof email !== 'string') { + return { valid: false, error: 'Email invalide' }; + } + + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(email)) { + return { valid: false, error: 'Format d\'email invalide' }; + } + + if (email.length > 254) { + return { valid: false, error: 'Email trop long' }; + } + + return { valid: true }; +}; + +/** + * Nettoie rĂ©cursivement un objet + */ +const sanitizeObject = (obj) => { + if (obj === null || obj === undefined) return obj; + + if (typeof obj === 'string') { + return sanitizeString(obj); + } + + if (Array.isArray(obj)) { + return obj.map(item => sanitizeObject(item)); + } + + if (typeof obj === 'object') { + const sanitized = {}; + for (const [key, value] of Object.entries(obj)) { + sanitized[key] = sanitizeObject(value); + } + return sanitized; + } + + return obj; +}; + +/** + * Middleware principal de validation + */ +const inputValidationMiddleware = (req, res, next) => { + try { + // Log des requĂȘtes suspectes + const suspiciousPatterns = [ + / - - +