Update v1.2.0-beta - Dynamic context menu & permissions
All checks were successful
continuous-integration/drone/push Build is passing

 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
This commit is contained in:
2025-10-25 23:55:51 +02:00
parent 58b57fbb84
commit 2df1b28962
33 changed files with 6275 additions and 1462 deletions

View File

@@ -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
};