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,8 @@
{
"permissions": {
"allow": [
"Bash(npm audit:*)"
],
"deny": []
}
}

234
CLAUDE.md Normal file
View File

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

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

View File

@@ -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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#x27;')
.replace(/\//g, '&#x2F;');
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 = [
/<script/i,
/javascript:/i,
/on\w+\s*=/i,
/\.\.\//,
/union\s+select/i,
/insert\s+into/i,
/delete\s+from/i,
/drop\s+table/i,
/exec\s*\(/i,
/eval\s*\(/i
];
const checkSuspicious = (value) => {
if (typeof value === 'string') {
return suspiciousPatterns.some(pattern => pattern.test(value));
}
return false;
};
// Vérifier le body
if (req.body && typeof req.body === 'object') {
const bodyStr = JSON.stringify(req.body);
if (suspiciousPatterns.some(pattern => pattern.test(bodyStr))) {
logger.warn(`Suspicious input detected in request body from ${req.ip}: ${req.path}`);
}
}
// Vérifier les query params
if (req.query && typeof req.query === 'object') {
const queryStr = JSON.stringify(req.query);
if (suspiciousPatterns.some(pattern => pattern.test(queryStr))) {
logger.warn(`Suspicious input detected in query params from ${req.ip}: ${req.path}`);
}
}
// Continuer sans bloquer (logging only pour ne pas casser l'app)
next();
} catch (error) {
logger.error('Error in input validation middleware:', error);
next();
}
};
module.exports = {
inputValidationMiddleware,
sanitizeString,
sanitizeObject,
validateFileName,
validatePath,
validateUserId,
validateEmail
};

View File

@@ -0,0 +1,194 @@
/**
* Middleware de rate limiting renforcé
* Protège contre les abus et les attaques par force brute
*/
const rateLimit = require('express-rate-limit');
const { logger } = require('../config/logs');
/**
* Rate limiter général pour toutes les requêtes
*/
const generalLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 1000, // Limite de 1000 requêtes par fenêtre
message: {
error: 'Trop de requêtes depuis cette adresse IP, veuillez réessayer plus tard.',
retryAfter: '15 minutes'
},
standardHeaders: true,
legacyHeaders: false,
skip: (req) => {
// Skip pour localhost
return req.ip === '127.0.0.1' || req.ip === '::1' || req.ip === '::ffff:127.0.0.1';
},
handler: (req, res) => {
logger.warn(`Rate limit exceeded for ${req.ip} on ${req.path}`);
res.status(429).json({
error: 'Trop de requêtes',
message: 'Vous avez dépassé la limite de requêtes autorisées. Veuillez réessayer dans 15 minutes.',
retryAfter: 900
});
}
});
/**
* Rate limiter strict pour les endpoints sensibles (authentification)
*/
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // Seulement 5 tentatives de connexion
skipSuccessfulRequests: true, // Ne pas compter les tentatives réussies
message: {
error: 'Trop de tentatives de connexion, votre compte est temporairement verrouillé.',
retryAfter: '15 minutes'
},
standardHeaders: true,
legacyHeaders: false,
skip: (req) => {
return req.ip === '127.0.0.1' || req.ip === '::1' || req.ip === '::ffff:127.0.0.1';
},
handler: (req, res) => {
logger.warn(`Auth rate limit exceeded for ${req.ip} on ${req.path}`);
res.status(429).json({
error: 'Compte temporairement verrouillé',
message: 'Trop de tentatives de connexion. Veuillez réessayer dans 15 minutes.',
retryAfter: 900
});
}
});
/**
* Rate limiter pour les uploads
*/
const uploadLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 heure
max: 100, // 100 uploads par heure
message: {
error: 'Limite d\'upload atteinte',
retryAfter: '1 heure'
},
standardHeaders: true,
legacyHeaders: false,
skip: (req) => {
return req.ip === '127.0.0.1' || req.ip === '::1' || req.ip === '::ffff:127.0.0.1';
},
handler: (req, res) => {
logger.warn(`Upload rate limit exceeded for ${req.ip}`);
res.status(429).json({
error: 'Limite d\'upload dépassée',
message: 'Vous avez atteint la limite d\'uploads autorisés. Veuillez réessayer dans 1 heure.',
retryAfter: 3600
});
}
});
/**
* Rate limiter pour les API
*/
const apiLimiter = rateLimit({
windowMs: 1 * 60 * 1000, // 1 minute
max: 60, // 60 requêtes par minute
message: {
error: 'Limite d\'API atteinte',
retryAfter: '1 minute'
},
standardHeaders: true,
legacyHeaders: false,
skip: (req) => {
return req.ip === '127.0.0.1' || req.ip === '::1' || req.ip === '::ffff:127.0.0.1';
},
handler: (req, res) => {
logger.warn(`API rate limit exceeded for ${req.ip} on ${req.path}`);
res.status(429).json({
error: 'Limite d\'API dépassée',
message: 'Vous avez dépassé la limite de requêtes API autorisées. Veuillez réessayer dans 1 minute.',
retryAfter: 60
});
}
});
/**
* Rate limiter pour la création de dossiers/fichiers
*/
const createLimiter = rateLimit({
windowMs: 10 * 60 * 1000, // 10 minutes
max: 50, // 50 créations par 10 minutes
message: {
error: 'Limite de création atteinte',
retryAfter: '10 minutes'
},
standardHeaders: true,
legacyHeaders: false,
skip: (req) => {
return req.ip === '127.0.0.1' || req.ip === '::1' || req.ip === '::ffff:127.0.0.1';
},
handler: (req, res) => {
logger.warn(`Create rate limit exceeded for ${req.ip} on ${req.path}`);
res.status(429).json({
error: 'Limite de création dépassée',
message: 'Vous avez dépassé la limite de créations autorisées. Veuillez réessayer dans 10 minutes.',
retryAfter: 600
});
}
});
/**
* Rate limiter pour les suppressions
*/
const deleteLimiter = rateLimit({
windowMs: 5 * 60 * 1000, // 5 minutes
max: 30, // 30 suppressions par 5 minutes
message: {
error: 'Limite de suppression atteinte',
retryAfter: '5 minutes'
},
standardHeaders: true,
legacyHeaders: false,
skip: (req) => {
return req.ip === '127.0.0.1' || req.ip === '::1' || req.ip === '::ffff:127.0.0.1';
},
handler: (req, res) => {
logger.warn(`Delete rate limit exceeded for ${req.ip} on ${req.path}`);
res.status(429).json({
error: 'Limite de suppression dépassée',
message: 'Vous avez dépassé la limite de suppressions autorisées. Veuillez réessayer dans 5 minutes.',
retryAfter: 300
});
}
});
/**
* Rate limiter strict pour la recherche d'utilisateurs (prévient l'énumération)
*/
const userSearchLimiter = rateLimit({
windowMs: 1 * 60 * 1000, // 1 minute
max: 10, // 10 recherches par minute
message: {
error: 'Limite de recherche atteinte',
retryAfter: '1 minute'
},
standardHeaders: true,
legacyHeaders: false,
skip: (req) => {
return req.ip === '127.0.0.1' || req.ip === '::1' || req.ip === '::ffff:127.0.0.1';
},
handler: (req, res) => {
logger.warn(`User search rate limit exceeded for ${req.ip}`);
res.status(429).json({
error: 'Limite de recherche dépassée',
message: 'Vous avez dépassé la limite de recherches autorisées. Veuillez réessayer dans 1 minute.',
retryAfter: 60
});
}
});
module.exports = {
generalLimiter,
authLimiter,
uploadLimiter,
apiLimiter,
createLimiter,
deleteLimiter,
userSearchLimiter
};

View File

@@ -0,0 +1,76 @@
/**
* Middleware de sécurité pour ajouter les headers HTTP sécurisés
* Conforme aux bonnes pratiques OWASP
*/
const securityHeadersMiddleware = (req, res, next) => {
// Désactive l'envoi de l'en-tête X-Powered-By pour ne pas révéler la stack technique
res.removeHeader('X-Powered-By');
// Content Security Policy (CSP) - Protège contre les attaques XSS
res.setHeader(
'Content-Security-Policy',
[
"default-src 'self'",
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://code.jquery.com https://cdnjs.cloudflare.com https://maxcdn.bootstrapcdn.com https://cdn.jsdelivr.net https://cdn.tailwindcss.com",
"style-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com https://fonts.googleapis.com",
"img-src 'self' data: https: blob:",
"font-src 'self' https://cdnjs.cloudflare.com https://fonts.gstatic.com",
"connect-src 'self' ws: wss: https://cdnjs.cloudflare.com https://maxcdn.bootstrapcdn.com",
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'"
].join('; ')
);
// X-Content-Type-Options - Empêche le navigateur de deviner le type MIME
res.setHeader('X-Content-Type-Options', 'nosniff');
// X-Frame-Options - Protège contre le clickjacking
res.setHeader('X-Frame-Options', 'DENY');
// X-XSS-Protection - Active la protection XSS du navigateur
res.setHeader('X-XSS-Protection', '1; mode=block');
// Strict-Transport-Security (HSTS) - Force HTTPS
if (req.secure || process.env.NODE_ENV === 'production') {
res.setHeader(
'Strict-Transport-Security',
'max-age=31536000; includeSubDomains; preload'
);
}
// Referrer-Policy - Contrôle les informations de référence envoyées
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
// Permissions-Policy - Contrôle les fonctionnalités du navigateur
res.setHeader(
'Permissions-Policy',
[
'camera=()',
'microphone=()',
'geolocation=()',
'payment=()',
'usb=()',
'magnetometer=()',
'accelerometer=()',
'gyroscope=()'
].join(', ')
);
// X-Permitted-Cross-Domain-Policies - Restreint les politiques cross-domain
res.setHeader('X-Permitted-Cross-Domain-Policies', 'none');
// Cross-Origin-Embedder-Policy - Désactivé pour permettre les ressources externes (avatars, images CDN)
// res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp');
// Cross-Origin-Opener-Policy - Isole le contexte de navigation
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
// Cross-Origin-Resource-Policy - Contrôle le partage de ressources
res.setHeader('Cross-Origin-Resource-Policy', 'same-origin');
next();
};
module.exports = securityHeadersMiddleware;

View File

@@ -2,7 +2,7 @@ version: '3.9'
services: services:
cdn-app-insider: cdn-app-insider:
container_name: cdn-app-insider container_name: cdn-app-insider
image: swiftlogiclabs/cdn-app-insider image: swiftlogiclabs/cdn-app-insider:1.2.0-beta
ports: ports:
- 5053:5053 - 5053:5053
volumes: volumes:

725
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "@cdn-app/insider-myaxrin-labs-dinawo", "name": "@cdn-app/insider-myaxrin-labs-dinawo",
"version": "1.1.1-beta.1", "version": "1.2.0-beta",
"description": "", "description": "",
"main": "server.js", "main": "server.js",
"scripts": { "scripts": {

457
public/css/admin.styles.css Normal file
View File

@@ -0,0 +1,457 @@
/**
* Styles unifiés pour le centre d'administration
* Design moderne et cohérent avec le profil utilisateur
*/
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
/* Variables CSS identiques au profil */
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
}
* {
box-sizing: border-box;
}
body.admin-page {
font-family: 'Inter', sans-serif;
background-color: hsl(var(--background));
color: hsl(var(--foreground));
transition: background-color 0.3s ease, color 0.3s ease;
margin: 0;
padding: 0;
min-height: 100vh;
}
/* Backdrop avec blur */
.backdrop {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.3);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
z-index: 1;
}
/* Container principal */
.admin-container {
position: relative;
z-index: 2;
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
min-height: 100vh;
}
/* Header de l'admin */
.admin-header {
background: hsl(var(--card));
border-radius: 20px;
padding: 2rem;
margin-bottom: 2rem;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
border: 1px solid hsl(var(--border));
}
.admin-header h1 {
font-size: 2rem;
font-weight: 700;
color: hsl(var(--foreground));
margin: 0 0 0.5rem 0;
display: flex;
align-items: center;
gap: 1rem;
}
.admin-header h1 i {
color: hsl(var(--primary));
font-size: 1.75rem;
}
.admin-header p {
color: hsl(var(--muted-foreground));
margin: 0;
font-size: 1rem;
}
/* Grille de cartes */
.admin-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
/* Carte admin */
.admin-card {
background: hsl(var(--card));
border-radius: 16px;
padding: 1.5rem;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
border: 1px solid hsl(var(--border));
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
}
.admin-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, hsl(var(--primary)), hsl(var(--accent)));
transform: scaleX(0);
transition: transform 0.3s ease;
}
.admin-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);
}
.admin-card:hover::before {
transform: scaleX(1);
}
.admin-card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1rem;
}
.admin-card-title {
font-size: 1.25rem;
font-weight: 600;
color: hsl(var(--foreground));
margin: 0;
display: flex;
align-items: center;
gap: 0.75rem;
}
.admin-card-title i {
font-size: 1.5rem;
color: hsl(var(--primary));
}
.admin-card-badge {
background: hsl(var(--primary));
color: hsl(var(--primary-foreground));
padding: 0.25rem 0.75rem;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 600;
}
.admin-card-body {
color: hsl(var(--muted-foreground));
line-height: 1.6;
}
.admin-card-footer {
margin-top: 1.5rem;
padding-top: 1.5rem;
border-top: 1px solid hsl(var(--border));
display: flex;
gap: 0.75rem;
}
/* Boutons */
.btn-admin {
padding: 0.75rem 1.5rem;
border-radius: 12px;
font-weight: 600;
font-size: 0.95rem;
border: none;
cursor: pointer;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
gap: 0.5rem;
text-decoration: none;
}
.btn-admin-primary {
background: hsl(var(--primary));
color: hsl(var(--primary-foreground));
}
.btn-admin-primary:hover {
background: hsl(var(--primary) / 0.9);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.btn-admin-secondary {
background: hsl(var(--secondary));
color: hsl(var(--secondary-foreground));
}
.btn-admin-secondary:hover {
background: hsl(var(--accent));
transform: translateY(-2px);
}
.btn-admin-danger {
background: hsl(var(--destructive));
color: hsl(var(--destructive-foreground));
}
.btn-admin-danger:hover {
background: hsl(var(--destructive) / 0.9);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3);
}
/* Tableaux */
.admin-table-container {
background: hsl(var(--card));
border-radius: 16px;
padding: 1.5rem;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
border: 1px solid hsl(var(--border));
overflow: hidden;
}
.admin-table {
width: 100%;
border-collapse: collapse;
}
.admin-table thead tr {
background: hsl(var(--muted) / 0.5);
border-bottom: 2px solid hsl(var(--border));
}
.admin-table th {
padding: 1rem;
text-align: left;
font-weight: 600;
color: hsl(var(--foreground));
font-size: 0.875rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.admin-table td {
padding: 1rem;
border-bottom: 1px solid hsl(var(--border));
color: hsl(var(--foreground));
}
.admin-table tbody tr {
transition: background-color 0.2s ease;
}
.admin-table tbody tr:hover {
background: hsl(var(--muted) / 0.3);
}
/* Formulaires */
.admin-form-group {
margin-bottom: 1.5rem;
}
.admin-form-label {
display: block;
font-weight: 600;
color: hsl(var(--foreground));
margin-bottom: 0.5rem;
font-size: 0.95rem;
}
.admin-form-input,
.admin-form-select,
.admin-form-textarea {
width: 100%;
padding: 0.75rem 1rem;
border: 1px solid hsl(var(--border));
border-radius: 8px;
background: hsl(var(--background));
color: hsl(var(--foreground));
font-size: 0.95rem;
transition: all 0.2s ease;
}
.admin-form-input:focus,
.admin-form-select:focus,
.admin-form-textarea:focus {
outline: none;
border-color: hsl(var(--primary));
box-shadow: 0 0 0 3px hsl(var(--primary) / 0.1);
}
/* Stats cards */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.stat-card {
background: hsl(var(--card));
border-radius: 16px;
padding: 1.5rem;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
border: 1px solid hsl(var(--border));
position: relative;
overflow: hidden;
}
.stat-card-icon {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
margin-bottom: 1rem;
}
.stat-card-icon.primary {
background: hsl(var(--primary) / 0.1);
color: hsl(var(--primary));
}
.stat-card-icon.success {
background: rgba(16, 185, 129, 0.1);
color: #10b981;
}
.stat-card-icon.warning {
background: rgba(245, 158, 11, 0.1);
color: #f59e0b;
}
.stat-card-icon.danger {
background: hsl(var(--destructive) / 0.1);
color: hsl(var(--destructive));
}
.stat-card-value {
font-size: 2rem;
font-weight: 700;
color: hsl(var(--foreground));
margin: 0.5rem 0;
}
.stat-card-label {
color: hsl(var(--muted-foreground));
font-size: 0.875rem;
font-weight: 500;
}
/* Badges */
.badge-admin {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.35rem 0.75rem;
border-radius: 12px;
font-size: 0.8rem;
font-weight: 600;
}
.badge-admin.success {
background: rgba(16, 185, 129, 0.1);
color: #10b981;
}
.badge-admin.warning {
background: rgba(245, 158, 11, 0.1);
color: #f59e0b;
}
.badge-admin.danger {
background: hsl(var(--destructive) / 0.1);
color: hsl(var(--destructive));
}
.badge-admin.info {
background: rgba(59, 130, 246, 0.1);
color: #3b82f6;
}
/* Responsive */
@media (max-width: 768px) {
.admin-container {
padding: 1rem;
}
.admin-header h1 {
font-size: 1.5rem;
}
.admin-grid,
.stats-grid {
grid-template-columns: 1fr;
}
.admin-card-footer {
flex-direction: column;
}
}
/* Dark mode specific */
.dark .admin-card,
.dark .admin-table-container {
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.3);
}
.dark .admin-table thead tr {
background: hsl(var(--muted) / 0.3);
}
.dark .stat-card {
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.3);
}

View File

@@ -151,38 +151,60 @@
border-bottom: 1px solid hsl(var(--border)); border-bottom: 1px solid hsl(var(--border));
} }
.dropdown-menu { .dropdown-menu, .dropdown-menu.show {
display: none;
background-color: hsl(var(--card)); background-color: hsl(var(--card));
border: 1px solid hsl(var(--border)); border: 1px solid hsl(var(--border));
border-radius: var(--radius); border-radius: 16px !important;
box-shadow: 0 4px 8px rgba(0,0,0,0.1); box-shadow: 0 4px 16px rgba(0,0,0,0.18);
position: absolute; min-width: 180px;
top: 100%; padding: 0.5rem 0;
left: 50%; overflow: hidden;
transform: translateX(-50%);
z-index: 1000; z-index: 1000;
min-width: 150px; animation: menuFadeIn 0.12s ease-out;
}
.dropdown-menu.show {
display: block;
} }
.dropdown-item { .dropdown-item {
color: hsl(var(--foreground)); border-radius: 12px;
margin: 0 0.5rem;
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
text-decoration: none; font-size: 0.95rem;
display: block; display: flex;
align-items: center;
gap: 0.75rem;
color: hsl(var(--foreground));
background: none;
transition: background 0.12s, color 0.12s;
} }
.dropdown-item:hover { .dropdown-item:hover {
background-color: hsl(var(--muted)); background-color: hsl(var(--accent));
color: hsl(var(--foreground)); color: hsl(var(--accent-foreground));
}
.dropdown-item.text-danger {
color: hsl(var(--destructive));
}
.dropdown-item.text-danger:hover {
background-color: hsl(var(--destructive));
color: hsl(var(--destructive-foreground));
}
.dropdown-item.text-warning {
color: #f59e0b;
}
.dropdown-item.text-warning:hover {
background-color: #f59e0b22;
color: #fff;
} }
.dropdown-divider { .dropdown-divider {
border-top: 1px solid hsl(var(--border)); border-top: 1px solid hsl(var(--border));
margin: 0.25rem 0;
}
/* Correction du positionnement dynamique (haut/bas) */
.dropdown-menu[style*="bottom: 100%"] {
border-radius: 16px 16px 16px 16px !important;
box-shadow: 0 -4px 16px rgba(0,0,0,0.18);
} }
.footer { .footer {
@@ -192,70 +214,128 @@
padding: 1rem 0; padding: 1rem 0;
} }
.modal { .modal,
display: none; .modal.fade {
position: fixed; display: none !important;
top: 0; position: fixed !important;
left: 0; top: 0 !important;
width: 100%; left: 0 !important;
height: 100%; width: 100% !important;
background-color: rgba(0, 0, 0, 0.5); height: 100% !important;
justify-content: center; background-color: rgba(0, 0, 0, 0.6) !important;
align-items: center; backdrop-filter: blur(8px) !important;
z-index: 1000; justify-content: center !important;
align-items: center !important;
z-index: 1050 !important;
animation: fadeIn 0.2s ease !important;
} }
.modal.show { .modal.show,
display: flex; .modal.fade.show {
display: flex !important;
}
.modal-dialog {
position: relative !important;
margin: 0 auto !important;
max-width: 90% !important;
width: 600px !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
}
.modal-dialog-centered {
display: flex !important;
align-items: center !important;
min-height: calc(100% - 1rem) !important;
} }
.modal-content { .modal-content {
background-color: hsl(var(--card)); background-color: hsl(var(--card)) !important;
border-radius: 15px; border-radius: 16px !important;
box-shadow: 0 5px 15px rgba(0,0,0,0.3); box-shadow: 0 20px 60px rgba(0,0,0,0.4) !important;
padding: 20px; padding: 0 !important;
max-width: 90%; max-width: 100% !important;
width: 600px; width: 100% !important;
} border: 1px solid hsl(var(--border)) !important;
animation: modalSlideIn 0.3s ease !important;
.modal-header, .modal-body, .modal-footer {
padding: 20px;
} }
.modal-header { .modal-header {
border-bottom: none; padding: 1.5rem 2rem !important;
border-bottom: 1px solid hsl(var(--border)) !important;
display: flex !important;
align-items: center !important;
justify-content: space-between !important;
}
.modal-body {
padding: 2rem !important;
max-height: 70vh !important;
overflow-y: auto !important;
} }
.modal-footer { .modal-footer {
border-top: none; padding: 1rem 2rem !important;
display: flex; border-top: 1px solid hsl(var(--border)) !important;
justify-content: flex-end; display: flex !important;
gap: 0.5rem; justify-content: flex-end !important;
gap: 0.75rem !important;
}
.modal .modal-title {
font-size: 1.25rem !important;
font-weight: 600 !important;
color: hsl(var(--foreground)) !important;
margin: 0 !important;
} }
.modal .close { .modal .close {
font-size: 28px; font-size: 1.5rem !important;
opacity: 0.6; opacity: 0.6 !important;
transition: opacity 0.3s ease; transition: all 0.2s ease !important;
background: none !important;
border: none !important;
color: hsl(var(--foreground)) !important;
cursor: pointer !important;
padding: 0.5rem !important;
border-radius: 0.5rem !important;
} }
.modal .close:hover { .modal .close:hover {
opacity: 1; opacity: 1 !important;
background: hsl(var(--accent)) !important;
} }
.navbar { @keyframes fadeIn {
padding: 0.5rem 1rem; from { opacity: 0; }
to { opacity: 1; }
}
@keyframes modalSlideIn {
from {
opacity: 0;
transform: translateY(-20px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
} }
/* Alignement des boutons à droite */
.navbar { .navbar {
padding: 0.5rem 1rem !important; padding: 0.5rem 1rem !important;
min-height: 70px !important;
max-height: 70px !important;
position: relative !important;
} }
.container-fluid { .container-fluid {
display: flex !important; display: flex !important;
justify-content: space-between !important; justify-content: space-between !important;
align-items: center !important; align-items: center !important;
height: 100% !important;
} }
/* Alignement des boutons à droite */ /* Alignement des boutons à droite */
@@ -263,18 +343,21 @@ align-items: center !important;
display: flex !important; display: flex !important;
align-items: center !important; align-items: center !important;
margin-left: auto !important; margin-left: auto !important;
gap: 0.5rem !important;
position: relative !important;
} }
.navbar-nav .nav-item { .navbar-nav .nav-item {
margin-left: 10px !important; position: relative !important;
} }
/* Style pour les boutons de la navbar */ /* Style pour les boutons de la navbar */
.nav-btn { .nav-btn {
padding: 0.375rem 0.75rem !important; padding: 0.5rem 1rem !important;
font-size: 1rem !important; font-size: 0.95rem !important;
line-height: 1.5 !important; line-height: 1.5 !important;
border-radius: 0.25rem !important; border-radius: 0.5rem !important;
transition: all 0.2s ease !important;
} }
/* Style pour l'avatar de l'utilisateur */ /* Style pour l'avatar de l'utilisateur */
@@ -283,6 +366,13 @@ width: 40px !important;
height: 40px !important; height: 40px !important;
object-fit: cover !important; object-fit: cover !important;
border-radius: 50% !important; border-radius: 50% !important;
border: 2px solid hsl(var(--border)) !important;
transition: all 0.2s ease !important;
}
.user-avatar:hover {
transform: scale(1.05) !important;
border-color: hsl(var(--primary)) !important;
} }
/* Style pour le bouton du dropdown */ /* Style pour le bouton du dropdown */
@@ -290,17 +380,23 @@ border-radius: 50% !important;
padding: 0 !important; padding: 0 !important;
border: none !important; border: none !important;
background: none !important; background: none !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
} }
/* Style pour le menu dropdown */ /* Style pour le menu dropdown - IMPORTANT: position absolue */
.dropdown-menu { .navbar .dropdown {
position: relative !important;
}
.navbar .dropdown-menu {
position: absolute !important;
top: calc(100% + 0.5rem) !important;
right: 0 !important; right: 0 !important;
left: auto !important; left: auto !important;
} margin: 0 !important;
z-index: 1050 !important;
/* Assurez-vous que le dropdown est positionné correctement */
.dropdown {
position: relative !important;
} }
/* Ajustement pour les écrans plus petits */ /* Ajustement pour les écrans plus petits */
@@ -694,51 +790,66 @@ position: relative !important;
/* Styles pour le menu contextuel */ /* Styles pour le menu contextuel */
.context-menu { .context-menu {
position: fixed; position: fixed;
z-index: 1000; z-index: 1000 !important;
background-color: hsl(var(--card)); background-color: hsl(var(--card)) !important;
border: 1px solid hsl(var(--border)); border: 1px solid hsl(var(--border)) !important;
border-radius: var(--radius); border-radius: 12px !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); box-shadow: 0 10px 40px rgba(0, 0, 0, 0.4) !important;
min-width: 180px; min-width: 200px;
overflow: hidden; /* Pour que les hovers aillent jusqu'au bout */ overflow: hidden;
padding: 0.5rem;
} }
/* Style pour les items du menu */ /* Style pour les items du menu */
.menu-item, .menu-item,
.dropdown-item { .dropdown-item,
.context-menu button,
.context-menu a {
display: flex; display: flex;
align-items: center; align-items: center;
width: 100%; width: 100% !important;
padding: 0.5rem 1rem; padding: 0.75rem 1rem !important;
border: none; border: none !important;
background: none; background: none !important;
color: hsl(var(--foreground)); color: hsl(var(--foreground)) !important;
font-size: 0.875rem; font-size: 0.9rem !important;
text-align: left; text-align: left !important;
cursor: pointer; cursor: pointer !important;
transition: background-color 0.1s ease; transition: all 0.15s ease !important;
text-decoration: none; /* Pour les liens */ text-decoration: none !important;
white-space: nowrap; white-space: nowrap !important;
border-radius: 8px !important;
margin: 0.15rem 0 !important;
} }
/* Hover pour mode clair et sombre */ /* Hover pour mode clair et sombre */
.menu-item:hover, .menu-item:hover,
.dropdown-item:hover { .dropdown-item:hover,
background-color: hsl(var(--accent)); .context-menu button:hover,
.context-menu a:hover {
background-color: hsl(var(--accent)) !important;
color: hsl(var(--accent-foreground)) !important;
transform: translateX(2px);
} }
.dark .menu-item:hover, .dark .menu-item:hover,
.dark .dropdown-item:hover { .dark .dropdown-item:hover,
background-color: hsl(var(--accent)); .dark .context-menu button:hover,
color: hsl(var(--accent-foreground)); .dark .context-menu a:hover {
background-color: hsl(var(--accent)) !important;
color: hsl(var(--accent-foreground)) !important;
transform: translateX(2px);
} }
/* Alignement des icônes */ /* Alignement des icônes */
.menu-item i, .menu-item i,
.dropdown-item i { .dropdown-item i,
width: 20px; .context-menu button i,
margin-right: 0.75rem; .context-menu a i {
text-align: center; width: 20px !important;
margin-right: 0.75rem !important;
text-align: center !important;
font-size: 0.95rem !important;
} }
/* Style pour les items destructifs (suppression) */ /* Style pour les items destructifs (suppression) */
@@ -756,9 +867,10 @@ position: relative !important;
/* Style pour le séparateur */ /* Style pour le séparateur */
.menu-separator, .menu-separator,
.dropdown-divider { .dropdown-divider {
height: 1px; height: 1px !important;
background-color: hsl(var(--border)); background-color: hsl(var(--border)) !important;
margin: 0; margin: 0.5rem 0 !important;
border: none !important;
} }
/* Menu déroulant */ /* Menu déroulant */
@@ -816,126 +928,146 @@ tr[data-type="folder"]:active, tr[data-type="shared-folder"]:active {
background-color: hsl(var(--accent)); background-color: hsl(var(--accent));
} }
/* === VUE GRILLE REDESIGNÉE === */ /* === VUE GRILLE ULTRAMODERNE === */
.grid-view { .grid-view {
display: grid !important; display: grid !important;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)) !important;
gap: 1.25rem; gap: 1.25rem !important;
padding: 1.5rem; padding: 1.5rem !important;
width: 100%; width: 100% !important;
max-width: 100%; max-width: 100% !important;
box-sizing: border-box; box-sizing: border-box !important;
animation: gridFadeIn 0.3s ease-out; animation: gridFadeIn 0.4s ease-out !important;
justify-content: start;
align-content: start;
} }
.grid-view > tr { .grid-view > tr {
display: block; display: flex !important;
position: relative; flex-direction: column !important;
background: hsl(var(--card)); align-items: center !important;
border: 1px solid hsl(var(--border)); justify-content: flex-start !important;
border-radius: 12px; position: relative !important;
padding: 0; background: hsl(var(--card)) !important;
text-align: center; border: 1px solid hsl(var(--border)) !important;
cursor: pointer; border-radius: 16px !important;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); padding: 1.5rem 1rem !important;
height: auto; text-align: center !important;
overflow: hidden; cursor: pointer !important;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1) !important;
max-width: 250px; min-height: 160px !important;
min-width: 180px; overflow: visible !important;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06) !important;
} }
.grid-view tr:hover { .grid-view tr:hover {
transform: translateY(-4px); transform: translateY(-6px) scale(1.02) !important;
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15); box-shadow: 0 12px 35px rgba(0, 0, 0, 0.18) !important;
border-color: hsl(var(--primary) / 0.3); border-color: hsl(var(--primary) / 0.5) !important;
} }
.grid-view td { .grid-view td {
display: block; display: block !important;
padding: 0 !important; padding: 0 !important;
border: none !important; border: none !important;
} }
.grid-view td:not(:first-child) { .grid-view td:not(:first-child) {
display: none; display: none !important;
} }
.grid-view .icon-container { .grid-view .icon-container {
display: flex; display: flex !important;
flex-direction: column; flex-direction: column !important;
align-items: center; align-items: center !important;
gap: 0.75rem; justify-content: center !important;
padding: 1.5rem 1rem 1rem; gap: 1rem !important;
position: relative; padding: 0 !important;
position: relative !important;
width: 100% !important;
} }
.grid-view .icon { .grid-view .icon {
width: 56px; width: 72px !important;
height: 56px; height: 72px !important;
display: flex; display: flex !important;
align-items: center; align-items: center !important;
justify-content: center; justify-content: center !important;
background: linear-gradient(135deg, hsl(var(--primary) / 0.1), hsl(var(--primary) / 0.05)); background: linear-gradient(135deg, hsl(var(--primary) / 0.12), hsl(var(--accent) / 0.08)) !important;
border-radius: 12px; border-radius: 16px !important;
margin-bottom: 0.5rem; margin-bottom: 0 !important;
transition: transform 0.2s ease; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05) !important;
} }
.grid-view tr:hover .icon { .grid-view tr:hover .icon {
transform: scale(1.1); transform: scale(1.15) rotate(-5deg) !important;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.12) !important;
} }
.grid-view .icon i { .grid-view .icon i {
font-size: 1.75rem; font-size: 2.25rem !important;
color: hsl(var(--primary)); color: hsl(var(--primary)) !important;
transition: all 0.2s ease !important;
} }
.grid-view .icon i.fa-folder { .grid-view .icon i.fa-folder,
color: #f59e0b; .grid-view .icon i.fa-folder-open {
color: #f59e0b !important;
} }
.grid-view .icon i.fa-file { .grid-view .icon i.fa-file {
color: #3b82f6; color: #3b82f6 !important;
} }
.grid-view .icon i.fa-image { .grid-view .icon i.fa-image {
color: #10b981; color: #10b981 !important;
} }
.grid-view .icon i.fa-film { .grid-view .icon i.fa-film {
color: #ef4444; color: #ef4444 !important;
} }
.grid-view .icon i.fa-file-pdf { .grid-view .icon i.fa-file-pdf {
color: #dc2626; color: #dc2626 !important;
}
.grid-view .icon i.fa-file-archive {
color: #8b5cf6 !important;
}
.grid-view .icon i.fa-file-code {
color: #06b6d4 !important;
} }
.grid-view .label { .grid-view .label {
font-size: 0.9rem; font-size: 0.875rem !important;
font-weight: 600; font-weight: 600 !important;
color: hsl(var(--foreground)); color: hsl(var(--foreground)) !important;
margin: 0; margin: 0 !important;
overflow: hidden; overflow: hidden !important;
text-overflow: ellipsis; text-overflow: ellipsis !important;
display: -webkit-box; display: -webkit-box !important;
-webkit-line-clamp: 2; -webkit-line-clamp: 2 !important;
line-clamp: 2; line-clamp: 2 !important;
-webkit-box-orient: vertical; -webkit-box-orient: vertical !important;
max-width: 100%; max-width: 100% !important;
line-height: 1.3; line-height: 1.4 !important;
min-height: 2.6rem; min-height: 2.4rem !important;
text-align: center !important;
word-break: break-word !important;
padding: 0 0.5rem !important;
} }
.grid-view .details { .grid-view .details {
font-size: 0.75rem; font-size: 0.7rem !important;
color: hsl(var(--muted-foreground)); color: hsl(var(--muted-foreground)) !important;
margin-top: 0.25rem; margin: 0 !important;
padding: 0.5rem 0; padding: 0.4rem 0.75rem !important;
border-top: 1px solid hsl(var(--border) / 0.5); background: hsl(var(--muted) / 0.4) !important;
width: 100%; border-radius: 12px !important;
width: auto !important;
text-align: center !important;
font-weight: 500 !important;
border: 1px solid hsl(var(--border) / 0.5) !important;
} }
/* Actions pour la vue grille */ /* Actions pour la vue grille */
@@ -1191,17 +1323,13 @@ tr[data-type="folder"]:active, tr[data-type="shared-folder"]:active {
background: hsl(var(--card)); background: hsl(var(--card));
border: 1px solid hsl(var(--border)); border: 1px solid hsl(var(--border));
border-radius: 16px; border-radius: 16px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15); box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25), 0 0 0 1px rgba(255, 255, 255, 0.05);
max-width: 600px; padding: 24px;
width: 90%; max-width: 90%;
max-height: 90vh; width: 600px;
position: relative;
overflow: hidden; overflow: hidden;
transform: scale(0.9) translateY(20px); animation: contentShow 0.15s cubic-bezier(0.16, 1, 0.3, 1);
transition: transform 0.3s ease;
}
.collaboration-modal.show .modal-content {
transform: scale(1) translateY(0);
} }
.collaboration-modal .modal-header { .collaboration-modal .modal-header {

View File

@@ -0,0 +1,12 @@
/* Dropdown fixes for dashboard and folder views */
/* Fix pour les dropdowns qui ne s'affichent pas correctement */
.dropdown-menu {
z-index: 1050;
}
/* Fix pour les menus contextuels */
.context-menu {
position: fixed;
z-index: 9999;
}

View File

@@ -61,52 +61,141 @@ function initializeContextMenu() {
adjustMenuOptions(selectedItem); adjustMenuOptions(selectedItem);
showContextMenu(e.pageX, e.pageY); showContextMenu(e.pageX, e.pageY);
}); }); // Fermer le menu au clic extérieur
// Fermer le menu au clic extérieur
document.addEventListener('click', function(e) { document.addEventListener('click', function(e) {
const contextMenu = document.getElementById('contextMenu');
if (!contextMenu?.contains(e.target)) { if (!contextMenu?.contains(e.target)) {
hideContextMenu(); hideContextMenu();
} }
}); });// Actions du menu contextuel
document.addEventListener('click', function(e) {
if (!e.target.closest('#contextMenu')) return;
// Actions du menu e.preventDefault();
document.querySelectorAll('.context-menu .menu-item').forEach(item => { const target = e.target.closest('button, a');
item.addEventListener('click', function(e) { if (!target || !selectedItem) return;
e.preventDefault();
const action = this.dataset.action; let action = '';
if (selectedItem) { if (target.classList.contains('context-item-open')) action = 'open';
handleMenuAction(action, selectedItem); else if (target.classList.contains('context-item-rename')) action = 'rename';
} else if (target.classList.contains('context-item-collaborate')) action = 'collaborate';
else if (target.classList.contains('context-item-share')) action = 'copy-link';
else if (target.classList.contains('context-item-move')) action = 'move';
else if (target.classList.contains('context-item-leave')) action = 'leave';
else if (target.classList.contains('context-item-delete')) action = 'delete';
if (action) {
handleMenuAction(action, selectedItem);
hideContextMenu(); hideContextMenu();
}
}); function adjustMenuOptions(item) {
const contextMenu = document.getElementById('contextMenu');
if (!contextMenu) return;
const isFile = item.type === 'file';
const isFolder = item.type === 'folder';
const isSharedFolder = item.type === 'shared-folder';
// DEBUG
console.log('🔍 Context Menu Debug:', {
itemType: item.type,
itemName: item.name,
isFile,
isFolder,
isSharedFolder
}); });
});
function adjustMenuOptions(item) { // Vérifier si l'utilisateur est propriétaire (pour dossiers partagés)
const menuItems = contextMenu.querySelectorAll('.menu-item'); let isOwner = false;
if (isSharedFolder) {
const ownerCell = item.element.querySelector('td:nth-child(3) .text-muted');
isOwner = ownerCell && ownerCell.textContent.trim() === 'moi';
console.log('🔍 Shared Folder - isOwner:', isOwner);
}
menuItems.forEach(menuItem => { // Récupérer tous les éléments du menu
const action = menuItem.dataset.action; const openBtn = contextMenu.querySelector('.context-item-open');
const renameBtn = contextMenu.querySelector('.context-item-rename');
const collaborateBtn = contextMenu.querySelector('.context-item-collaborate');
const shareBtn = contextMenu.querySelector('.context-item-share');
const moveBtn = contextMenu.querySelector('.context-item-move');
const leaveBtn = contextMenu.querySelector('.context-item-leave');
const separator = contextMenu.querySelector('.menu-separator');
const deleteBtn = contextMenu.querySelector('.context-item-delete');
switch(action) { // MASQUER TOUT PAR DÉFAUT
case 'open': if (openBtn) openBtn.style.display = 'none';
menuItem.style.display = item.type.includes('folder') ? 'flex' : 'none'; if (renameBtn) renameBtn.style.display = 'none';
break; if (collaborateBtn) collaborateBtn.style.display = 'none';
case 'collaborate': if (shareBtn) shareBtn.style.display = 'none';
menuItem.style.display = item.type === 'folder' ? 'flex' : 'none'; if (moveBtn) moveBtn.style.display = 'none';
const collabBtn = item.element.querySelector('.toggle-collaboration-btn'); if (leaveBtn) leaveBtn.style.display = 'none';
const isCollaborative = collabBtn?.dataset.isCollaborative === 'true'; if (separator) separator.style.display = 'none';
menuItem.querySelector('span').textContent = if (deleteBtn) deleteBtn.style.display = 'none';
isCollaborative ? 'Gérer la collaboration' : 'Activer la collaboration';
break; // AFFICHER SELON LE TYPE
case 'copy-link': if (isFile) {
menuItem.style.display = item.type === 'file' ? 'flex' : 'none'; // FICHIERS : Renommer, Copier le lien, Déplacer, Supprimer
break; console.log('✅ Affichage menu FICHIER');
if (renameBtn) renameBtn.style.display = 'flex';
if (shareBtn) shareBtn.style.display = 'flex';
if (moveBtn) moveBtn.style.display = 'flex';
if (separator) separator.style.display = 'block';
if (deleteBtn) deleteBtn.style.display = 'flex';
} else if (isFolder) {
// DOSSIERS PERSONNELS : Ouvrir, Renommer, Collaborer, Supprimer
console.log('✅ Affichage menu DOSSIER');
if (openBtn) openBtn.style.display = 'flex';
if (renameBtn) renameBtn.style.display = 'flex';
if (collaborateBtn) {
collaborateBtn.style.display = 'flex';
// Vérifier si déjà collaboratif
const collabBadge = item.element.querySelector('.collaboration-badge');
const isCollaborative = collabBadge !== null;
const span = collaborateBtn.querySelector('span');
if (span) {
span.textContent = isCollaborative ? 'Gérer la collaboration' : 'Activer la collaboration';
}
} }
}); if (separator) separator.style.display = 'block';
} if (deleteBtn) deleteBtn.style.display = 'flex';
} else if (isSharedFolder) {
// DOSSIERS PARTAGÉS
console.log('✅ Affichage menu DOSSIER PARTAGÉ (owner:', isOwner, ')');
if (openBtn) openBtn.style.display = 'flex';
function showContextMenu(x, y) { if (isOwner) {
// PROPRIÉTAIRE : Renommer, Collaborer, Supprimer
if (renameBtn) renameBtn.style.display = 'flex';
if (collaborateBtn) {
collaborateBtn.style.display = 'flex';
const collabBadge = item.element.querySelector('.collaboration-badge');
const isCollaborative = collabBadge !== null;
const span = collaborateBtn.querySelector('span');
if (span) {
span.textContent = isCollaborative ? 'Gérer la collaboration' : 'Activer la collaboration';
}
}
if (separator) separator.style.display = 'block';
if (deleteBtn) deleteBtn.style.display = 'flex';
} else {
// INVITÉ : Quitter seulement
if (leaveBtn) leaveBtn.style.display = 'flex';
}
}
// DEBUG FINAL : afficher l'état de tous les boutons
console.log('📋 État final des boutons:', {
open: openBtn?.style.display,
rename: renameBtn?.style.display,
collaborate: collaborateBtn?.style.display,
share: shareBtn?.style.display,
move: moveBtn?.style.display,
leave: leaveBtn?.style.display,
delete: deleteBtn?.style.display
});
} function showContextMenu(x, y) {
const contextMenu = document.getElementById('contextMenu');
if (!contextMenu) return; if (!contextMenu) return;
contextMenu.style.display = 'block'; contextMenu.style.display = 'block';
@@ -128,6 +217,7 @@ function initializeContextMenu() {
} }
function hideContextMenu() { function hideContextMenu() {
const contextMenu = document.getElementById('contextMenu');
if (contextMenu) { if (contextMenu) {
contextMenu.style.display = 'none'; contextMenu.style.display = 'none';
} }
@@ -153,12 +243,15 @@ function initializeContextMenu() {
break; break;
case 'collaborate': case 'collaborate':
const collabBtn = item.element.querySelector('.toggle-collaboration-btn'); // Détecter si le dossier est déjà collaboratif en cherchant le badge
const isCollaborative = collabBtn?.dataset.isCollaborative === 'true'; const collabBadge = item.element.querySelector('.collaboration-badge');
const isCollaborative = collabBadge !== null;
if (isCollaborative) { if (isCollaborative) {
// Si déjà collaboratif, afficher les détails
showCollaborationDetails(item.name, item.type); showCollaborationDetails(item.name, item.type);
} else { } else {
// Sinon, activer la collaboration
toggleCollaboration(item.name, item.type, true); toggleCollaboration(item.name, item.type, true);
} }
break; break;
@@ -178,6 +271,12 @@ function initializeContextMenu() {
} }
break; break;
case 'leave':
if (item.type === 'shared-folder') {
leaveSharedFolder(item.name, item.element);
}
break;
case 'delete': case 'delete':
if (item.type === 'folder') { if (item.type === 'folder') {
confirmDeleteFolder(item.name); confirmDeleteFolder(item.name);
@@ -207,7 +306,50 @@ function initializeDropdowns() {
// Toggle le dropdown actuel // Toggle le dropdown actuel
const dropdown = this.nextElementSibling; const dropdown = this.nextElementSibling;
if (dropdown && dropdown.classList.contains('dropdown-menu')) { if (dropdown && dropdown.classList.contains('dropdown-menu')) {
// Correction du positionnement pour éviter le débordement en bas
dropdown.classList.toggle('show'); dropdown.classList.toggle('show');
if (dropdown.classList.contains('show')) {
// Reset
dropdown.style.top = '';
dropdown.style.bottom = '';
dropdown.style.transform = '';
const rect = dropdown.getBoundingClientRect();
const windowHeight = window.innerHeight;
if (rect.bottom > windowHeight) {
// Afficher au-dessus si déborde
dropdown.style.top = 'auto';
dropdown.style.bottom = '100%';
dropdown.style.transform = 'translateY(-8px)';
} else {
dropdown.style.top = '';
dropdown.style.bottom = '';
dropdown.style.transform = '';
}
}
}
// Gestion dynamique des options selon le type/propriétaire
const tr = this.closest('tr[data-type]');
if (tr && dropdown) {
const type = tr.getAttribute('data-type');
const owner = tr.querySelector('td:nth-child(3) .text-muted')?.textContent?.trim();
// Pour les dossiers partagés, vérifier si on est propriétaire
const isOwner = owner === 'moi';
dropdown.querySelectorAll('.dropdown-item').forEach(item => {
const action = item.textContent.trim();
// Déplacer : seulement pour les fichiers
if (action.includes('Déplacer')) {
item.style.display = (type === 'file') ? '' : 'none';
}
// Renommer/Supprimer : pas pour dossier partagé non propriétaire
if ((action.includes('Renommer') || action.includes('Supprimer')) && type === 'shared-folder' && !isOwner) {
item.style.display = 'none';
}
// Quitter : seulement pour dossier partagé non propriétaire
if (action.includes('Quitter ce dossier')) {
item.style.display = (type === 'shared-folder' && !isOwner) ? '' : 'none';
}
});
} }
}); });
}); });
@@ -227,6 +369,41 @@ function initializeDropdowns() {
e.stopPropagation(); e.stopPropagation();
}); });
}); });
// Action pour quitter un dossier partagé
document.querySelectorAll('.leave-shared-folder-btn').forEach(btn => {
btn.addEventListener('click', function(e) {
e.preventDefault();
const folderName = this.getAttribute('data-folder-name');
const folderOwner = this.getAttribute('data-folder-owner');
Swal.fire({
title: 'Quitter ce dossier partagé ?',
text: 'Vous ne verrez plus ce dossier dans votre dashboard.',
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Quitter',
cancelButtonText: 'Annuler'
}).then((result) => {
if (result.isConfirmed) {
fetch(`/api/dpanel/sharedfolders/leave`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ folderName, folderOwner })
})
.then(res => res.json())
.then(data => {
if (data.success) {
showToast('success', 'Dossier quitté');
setTimeout(() => location.reload(), 800);
} else {
showToast('error', data.error || 'Erreur');
}
})
.catch(() => showToast('error', 'Erreur réseau'));
}
});
});
});
} }
// =================== VUE GRILLE =================== // =================== VUE GRILLE ===================
@@ -945,9 +1122,9 @@ function createNewFolder(folderName) {
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ folderName }) body: JSON.stringify({ folderName })
}) })
.then(response => response.json()) .then(async response => {
.then(result => { const result = await response.json();
if (result.success || response.ok) { if (response.ok && (result.success || result.message === 'Folder created successfully.')) {
showToast('success', 'Dossier créé avec succès'); showToast('success', 'Dossier créé avec succès');
setTimeout(() => location.reload(), 1500); setTimeout(() => location.reload(), 1500);
} else { } else {
@@ -956,7 +1133,7 @@ function createNewFolder(folderName) {
}) })
.catch(error => { .catch(error => {
console.error('Error:', error); console.error('Error:', error);
showToast('error', 'Erreur lors de la création du dossier'); showToast('error', error.message || 'Erreur lors de la création du dossier');
}); });
} }
@@ -1328,3 +1505,49 @@ window.displayMetadata = displayMetadata;
window.addCollaborator = addCollaborator; window.addCollaborator = addCollaborator;
window.removeCollaborator = removeCollaborator; window.removeCollaborator = removeCollaborator;
window.toggleCollaboration = toggleCollaboration; window.toggleCollaboration = toggleCollaboration;
// =================== FONCTIONS COLLABORATIVES ===================
function leaveSharedFolder(folderName, element) {
Swal.fire({
title: 'Quitter le dossier partagé',
text: `Êtes-vous sûr de vouloir quitter le dossier "${folderName}" ? Vous perdrez l'accès à ce dossier et ses fichiers.`,
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#f59e0b',
cancelButtonColor: '#6b7280',
confirmButtonText: 'Oui, quitter',
cancelButtonText: 'Annuler'
}).then((result) => {
if (result.isConfirmed) {
// Extraire le propriétaire depuis l'URL du dossier partagé
const url = element.dataset.url;
const urlParts = url.split('/');
const owner = urlParts[urlParts.indexOf('shared') + 1];
fetch('/api/dpanel/sharedfolders/leave', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
folderName: folderName,
folderOwner: owner
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
showToast('success', 'Vous avez quitté le dossier partagé');
// Supprimer la ligne du tableau
element.remove();
} else {
showToast('error', data.error || 'Erreur lors de la sortie du dossier');
}
})
.catch(error => {
console.error('Error:', error);
showToast('error', 'Erreur de connexion');
});
}
});
}

View File

@@ -14,22 +14,7 @@ function calculateFolderSize(contents) {
} }
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
const copyButtons = document.querySelectorAll('.copy-button'); // La fonctionnalité de copie de lien est maintenant gérée dans folder.ejs
copyButtons.forEach(copyButton => {
copyButton.addEventListener("click", () => {
const fileContainer = copyButton.closest('tr');
const fileLink = fileContainer.querySelector('.file-link');
fileLink.style.display = "block";
fileLink.select();
document.execCommand("copy");
fileLink.style.display = "none";
copyButton.textContent = "Lien copié !";
setTimeout(() => {
copyButton.textContent = "Copier le lien";
}, 2000);
});
});
const filterForm = document.getElementById('filterForm'); const filterForm = document.getElementById('filterForm');
const extensionFilter = document.getElementById('extensionFilter'); const extensionFilter = document.getElementById('extensionFilter');
@@ -44,6 +29,8 @@ document.addEventListener('DOMContentLoaded', function () {
isDarkMode = !isDarkMode; isDarkMode = !isDarkMode;
document.body.classList.toggle('dark-mode', isDarkMode); document.body.classList.toggle('dark-mode', isDarkMode);
if (!icon) return; // Vérifier que l'élément existe avant de l'utiliser
if (isDarkMode) { if (isDarkMode) {
icon.classList.remove('bi-brightness-high-fill'); icon.classList.remove('bi-brightness-high-fill');
icon.classList.add('bi-moon-fill'); icon.classList.add('bi-moon-fill');
@@ -58,6 +45,8 @@ document.addEventListener('DOMContentLoaded', function () {
function applyStyleMode() { function applyStyleMode() {
document.body.classList.toggle('dark-mode', isDarkMode); document.body.classList.toggle('dark-mode', isDarkMode);
if (!icon) return; // Vérifier que l'élément existe avant de l'utiliser
if (isDarkMode) { if (isDarkMode) {
icon.classList.remove('bi-brightness-high-fill'); icon.classList.remove('bi-brightness-high-fill');
icon.classList.add('bi-moon-fill'); icon.classList.add('bi-moon-fill');
@@ -72,15 +61,18 @@ document.addEventListener('DOMContentLoaded', function () {
darkModeMediaQuery.addListener(applyStyleMode); darkModeMediaQuery.addListener(applyStyleMode);
applyStyleMode(); applyStyleMode();
styleSwitcherButton.addEventListener('click', toggleDarkMode); if (styleSwitcherButton) {
styleSwitcherButton.addEventListener('click', toggleDarkMode);
}
filterForm.addEventListener('submit', function (event) { if (filterForm) {
event.preventDefault(); filterForm.addEventListener('submit', function (event) {
event.preventDefault();
const selectedExtension = extensionFilter.value.toLowerCase(); const selectedExtension = extensionFilter ? extensionFilter.value.toLowerCase() : '';
const searchQuery = fileSearchInput.value.toLowerCase(); const searchQuery = fileSearchInput ? fileSearchInput.value.toLowerCase() : '';
const fileList = document.querySelectorAll('tr[data-extension]'); const fileList = document.querySelectorAll('tr[data-extension]');
fileList.forEach(file => { fileList.forEach(file => {
const fileExtension = file.getAttribute('data-extension').toLowerCase(); const fileExtension = file.getAttribute('data-extension').toLowerCase();
@@ -95,7 +87,8 @@ document.addEventListener('DOMContentLoaded', function () {
file.style.display = 'none'; file.style.display = 'none';
} }
}); });
}); });
}
}); });
async function confirmDeleteFile(folderName, filename) { async function confirmDeleteFile(folderName, filename) {

View File

@@ -540,6 +540,13 @@ document.addEventListener('DOMContentLoaded', function() {
initForm(); initForm();
}); });
// Initialisation du formulaire
function initForm() {
// Le formulaire est déjà initialisé dans le DOMContentLoaded à la ligne 375
// Cette fonction est appelée pour des initialisations supplémentaires si nécessaire
console.log('Formulaire initialisé');
}
// Gestion du thème // Gestion du thème
function initTheme() { function initTheme() {
const themeSwitcher = document.getElementById('themeSwitcher'); const themeSwitcher = document.getElementById('themeSwitcher');

View File

@@ -172,7 +172,8 @@ router.delete('/:folderName', authenticateToken, (req, res) => {
return res.status(403).json({ error: 'You do not have permission to delete this folder.' }); return res.status(403).json({ error: 'You do not have permission to delete this folder.' });
} }
fs.rmdir(folderPath, { recursive: true }, (err) => { // Node.js 14+ : fs.rm remplace fs.rmdir (deprecated)
fs.rm(folderPath, { recursive: true, force: true }, (err) => {
if (err) { if (err) {
return res.status(500).json({ error: 'Error deleting the folder.' }); return res.status(500).json({ error: 'Error deleting the folder.' });
} }

View File

@@ -0,0 +1,36 @@
const express = require('express');
const fs = require('fs');
const path = require('path');
const router = express.Router();
const authMiddleware = require('../../../Middlewares/authMiddleware');
const { ErrorLogger } = require('../../../config/logs');
// Quitter un dossier partagé
router.post('/leave', authMiddleware, async (req, res) => {
try {
const userId = req.userData.name;
const { folderName, folderOwner } = req.body;
if (!folderName || !folderOwner) {
return res.status(400).json({ error: 'Missing folderName or folderOwner' });
}
const collabPath = path.join(__dirname, '../../../data', 'collaboration.json');
let collabData = JSON.parse(fs.readFileSync(collabPath, 'utf8'));
const itemId = `folder-${folderName}`;
if (!collabData.activeFiles[itemId]) {
return res.status(404).json({ error: 'Shared folder not found' });
}
// Retirer l'utilisateur de la liste des collaborateurs
collabData.activeFiles[itemId].activeUsers = collabData.activeFiles[itemId].activeUsers.filter(u => u.id !== userId);
// Si plus aucun utilisateur, on peut supprimer la collaboration
if (collabData.activeFiles[itemId].activeUsers.length === 0) {
delete collabData.activeFiles[itemId];
}
fs.writeFileSync(collabPath, JSON.stringify(collabData, null, 2), 'utf8');
return res.json({ success: true });
} catch (error) {
ErrorLogger.error('Error leaving shared folder:', error);
return res.status(500).json({ error: 'Erreur lors du quit du dossier partagé.' });
}
});
module.exports = router;

View File

@@ -31,7 +31,40 @@ router.post('/', (req, res) => {
} }
const file = files.file[0]; const file = files.file[0];
const userDir = path.join(process.cwd(), 'cdn-files', req.user.name); const targetFolder = fields.targetFolder ? fields.targetFolder[0] : '';
const isSharedFolder = fields.isSharedFolder ? fields.isSharedFolder[0] === 'true' : false;
const ownerName = fields.ownerName ? fields.ownerName[0] : '';
// Construction du chemin cible avec le dossier spécifié
let userName = req.user.name;
// Si c'est un dossier partagé, utiliser le nom du propriétaire
if (isSharedFolder && ownerName) {
// Vérifier les permissions de collaboration
const collaborationFilePath = path.join(__dirname, '../../../data', 'collaboration.json');
try {
const collaborationData = JSON.parse(await fs.promises.readFile(collaborationFilePath, 'utf8'));
const itemId = `folder-${targetFolder}`;
const folderInfo = collaborationData.activeFiles[itemId];
// Vérifier si l'utilisateur a accès au dossier partagé
if (!folderInfo || !folderInfo.isCollaborative ||
!folderInfo.activeUsers.some(u => u.id === req.user.id)) {
return res.status(403).send('Accès refusé au dossier partagé');
}
userName = ownerName;
} catch (error) {
console.error('Error checking collaboration permissions:', error);
return res.status(500).send('Erreur lors de la vérification des permissions');
}
}
let userDir = path.join(process.cwd(), 'cdn-files', userName);
if (targetFolder) {
userDir = path.join(userDir, targetFolder);
}
const filename = fields.filename ? fields.filename[0] : file.originalFilename; const filename = fields.filename ? fields.filename[0] : file.originalFilename;
const filePath = path.join(userDir, filename); const filePath = path.join(userDir, filename);

View File

@@ -50,7 +50,7 @@ router.get('/', authMiddleware, async (req, res) => {
}); });
Promise.all([Promise.all(reports)]).then(([completedReports]) => { Promise.all([Promise.all(reports)]).then(([completedReports]) => {
res.render('paramAdminPrivacy&Security', { users: User, reports: completedReports }); res.render('paramAdminPrivacy&Security', { user: user, users: User, reports: completedReports });
}); });
} catch (err) { } catch (err) {
console.error(err); console.error(err);

View File

@@ -61,7 +61,7 @@ router.get('/', authMiddleware, async (req, res) => {
}); });
Promise.all(logs).then(completed => { Promise.all(logs).then(completed => {
res.render('paramAdminStats&Logs', { users: User, setup: setup, uptime, memoryUsage, cpuUsage, logs: completed }); res.render('paramAdminStats&Logs', { user: user, users: User, setup: setup, uptime, memoryUsage, cpuUsage, logs: completed });
}); });
}); });
}); });

View File

@@ -45,7 +45,7 @@ router.get('/', authMiddleware, async (req, res) => {
let end = start + limit; let end = start + limit;
let usersForPage = users.slice(start, end); let usersForPage = users.slice(start, end);
res.render('paramAdminUser', { users: usersForPage, setup: setup, pages: pages, currentPage: currentPage, limit: limit }); res.render('paramAdminUser', { user: user, users: usersForPage, setup: setup, pages: pages, currentPage: currentPage, limit: limit });
} catch (err) { } catch (err) {
console.error(err); console.error(err);
res.status(500).send('Server Error'); res.status(500).send('Server Error');

View File

@@ -28,7 +28,6 @@ router.get('/', authMiddleware, async (req, res) => {
const data = fs.readFileSync(path.join(__dirname, '../../../data', 'user.json'), 'utf8'); const data = fs.readFileSync(path.join(__dirname, '../../../data', 'user.json'), 'utf8');
const users = JSON.parse(data); const users = JSON.parse(data);
const user = users.find(user => user.name === req.user.name); const user = users.find(user => user.name === req.user.name);
if (!user || user.role !== 'admin') { if (!user || user.role !== 'admin') {
@@ -36,7 +35,47 @@ router.get('/', authMiddleware, async (req, res) => {
return res.status(403).json({ message: "You do not have the necessary rights to access this resource." }); return res.status(403).json({ message: "You do not have the necessary rights to access this resource." });
} }
res.render('paramAdmin', { users: User, setup: setup }); // Calculer les stats pour le dashboard admin
let foldersCount = 0;
let totalSize = 0;
try {
const fileInfoData = fs.readFileSync(path.join(__dirname, '../../../data', 'file_info.json'), 'utf8');
const fileInfo = JSON.parse(fileInfoData);
// Compter les dossiers
users.forEach(u => {
const userFolderPath = path.join(__dirname, '../../../cdn-files', u.name);
if (fs.existsSync(userFolderPath)) {
const folders = fs.readdirSync(userFolderPath, { withFileTypes: true })
.filter(dirent => dirent.isDirectory());
foldersCount += folders.length;
}
});
// Calculer l'espace total utilisé
totalSize = fileInfo.reduce((sum, item) => sum + (item.size || 0), 0);
} catch (err) {
console.warn('Could not read file_info.json:', err.message);
}
// Stats système
const uptime = process.uptime();
const memoryUsage = process.memoryUsage();
const cpuUsage = process.cpuUsage();
res.render('paramAdmin', {
user: user,
users: User,
setup: setup,
stats: {
foldersCount,
totalSize,
uptime,
memoryUsage: Math.round(memoryUsage.heapUsed / 1024 / 1024), // MB
cpuUsage: Math.round((cpuUsage.user + cpuUsage.system) / 1000) // ms
}
});
} catch (err) { } catch (err) {
console.error(err); console.error(err);
res.status(500).send('Server Error'); res.status(500).send('Server Error');

View File

@@ -80,7 +80,11 @@ router.get('/shared/:ownerName/:folderName', authMiddleware, async (req, res) =>
const availableExtensions = Array.from(new Set(fileDetails.map(file => file.extension))); const availableExtensions = Array.from(new Set(fileDetails.map(file => file.extension)));
// Determine if current user is owner
const isOwner = folderInfo.activeUsers.length > 0 && folderInfo.activeUsers[0].id === userId;
res.render('folder', { res.render('folder', {
user: req.userData,
files: fileDetails, files: fileDetails,
folders, folders,
allFolders, allFolders,
@@ -90,7 +94,10 @@ router.get('/shared/:ownerName/:folderName', authMiddleware, async (req, res) =>
fileInfoNames, fileInfoNames,
userName, userName,
isSharedFolder: true, isSharedFolder: true,
ownerName ownerName,
isCollaborativeFolder: true,
isOwner,
currentUserId: userId
}); });
} catch (error) { } catch (error) {
@@ -196,10 +203,47 @@ router.get('/:folderName', authMiddleware, async (req, res) => {
}); });
Promise.all(fileDetailsPromises) Promise.all(fileDetailsPromises)
.then(fileDetails => { .then(async fileDetails => {
const availableExtensions = Array.from(new Set(fileDetails.map(file => file.extension))); const availableExtensions = Array.from(new Set(fileDetails.map(file => file.extension)));
res.render('folder', { files: fileDetails, folders, allFolders, extensions: availableExtensions, currentFolder: currentFolderName, folderName: folderName, fileInfoNames, userName }); // Check if current folder is collaborative
let isCollaborativeFolder = false;
let isOwner = true; // By default, user is owner of their own folder
let collaborators = [];
try {
const collaborationFilePath = path.join(__dirname, '../../../data', 'collaboration.json');
const collaborationData = JSON.parse(await fs.promises.readFile(collaborationFilePath, 'utf8'));
const itemId = `folder-${folderName}`;
const folderInfo = collaborationData.activeFiles[itemId];
if (folderInfo && folderInfo.isCollaborative && folderInfo.activeUsers) {
isCollaborativeFolder = true;
collaborators = folderInfo.activeUsers;
// First user in activeUsers array is the owner
if (collaborators.length > 0) {
isOwner = collaborators[0].id === userRealId;
}
}
} catch (error) {
// If collaboration.json doesn't exist or can't be read, continue without collaboration info
console.log('No collaboration data found:', error.message);
}
res.render('folder', {
user: user,
files: fileDetails,
folders,
allFolders,
extensions: availableExtensions,
currentFolder: currentFolderName,
folderName: folderName,
fileInfoNames,
userName,
isCollaborativeFolder,
isOwner,
currentUserId: userRealId
});
}) })
.catch(error => { .catch(error => {
console.error('Error processing file details:', error); console.error('Error processing file details:', error);

View File

@@ -116,7 +116,11 @@ router.get('/:userId/:filename', async (req, res) => {
await pipeline(readStream, res); await pipeline(readStream, res);
} }
} catch (err) { } catch (err) {
ErrorLogger.error('Error handling request:', err); // Ne pas logger les fermetures prématurées côté client (comportement normal)
// Cela se produit quand l'utilisateur annule le téléchargement, ferme le navigateur, etc.
if (err.code !== 'ERR_STREAM_PREMATURE_CLOSE' && err.code !== 'ECONNRESET' && err.code !== 'EPIPE') {
ErrorLogger.error('Error handling request:', err);
}
if (!res.headersSent) { if (!res.headersSent) {
res.status(500).send('Error reading file.'); res.status(500).send('Error reading file.');
} }

View File

@@ -30,6 +30,7 @@ const ProfilUser = require('./Dpanel/Dashboard/ProfilUser.js');
const PofilPictureRoute = require('./Dpanel/API/ProfilPicture.js'); const PofilPictureRoute = require('./Dpanel/API/ProfilPicture.js');
const CollaborationRoute = require('./Dpanel/API/Collaboration.js'); const CollaborationRoute = require('./Dpanel/API/Collaboration.js');
const UserSearchRoute = require('./Dpanel/API/UserSearch.js'); const UserSearchRoute = require('./Dpanel/API/UserSearch.js');
const SharedFoldersRoute = require('./Dpanel/API/SharedFolders.js');
const loginRoute = require('./Auth/Login.js'); const loginRoute = require('./Auth/Login.js');
const logoutRoute = require('./Auth/Logout.js'); const logoutRoute = require('./Auth/Logout.js');
@@ -78,6 +79,7 @@ router.use('/api/dpanel/dashboard/getfilefolder', getFileFolderRoute, logApiRequ
router.use('/api/dpanel/dashboard/profilpicture', PofilPictureRoute, logApiRequest); router.use('/api/dpanel/dashboard/profilpicture', PofilPictureRoute, logApiRequest);
router.use('/api/dpanel/collaboration', discordWebhookSuspisiousAlertMiddleware, logApiRequest, CollaborationRoute); router.use('/api/dpanel/collaboration', discordWebhookSuspisiousAlertMiddleware, logApiRequest, CollaborationRoute);
router.use('/api/dpanel/users/search', UserSearchRoute, logApiRequest); router.use('/api/dpanel/users/search', UserSearchRoute, logApiRequest);
router.use('/api/dpanel/sharedfolders', discordWebhookSuspisiousAlertMiddleware, logApiRequest, SharedFoldersRoute);
router.use('/auth/login', loginRoute); router.use('/auth/login', loginRoute);
router.use('/auth/logout', logoutRoute); router.use('/auth/logout', logoutRoute);

View File

@@ -17,6 +17,11 @@ const routes = require('./routes/routes.js');
const fileCleanup = require('./services/fileCleanupService'); const fileCleanup = require('./services/fileCleanupService');
const reportManager = require('./services/reportService.js'); const reportManager = require('./services/reportService.js');
// Import des middlewares de sécurité
const securityHeadersMiddleware = require('./Middlewares/securityHeadersMiddleware');
const { inputValidationMiddleware } = require('./Middlewares/inputValidationMiddleware');
const { generalLimiter } = require('./Middlewares/rateLimitMiddleware');
// Configuration de l'application // Configuration de l'application
const app = express(); const app = express();
const PORT = process.env.PORT || 5053; const PORT = process.env.PORT || 5053;
@@ -60,17 +65,28 @@ const loadSetup = async () => {
(req, res) => res.status(403).json({ error: 'Access Denied' })); (req, res) => res.status(403).json({ error: 'Access Denied' }));
// Configuration des middlewares // Configuration des middlewares
// Désactiver le header X-Powered-By
app.disable('x-powered-by');
// Middlewares de sécurité
app.use(securityHeadersMiddleware);
app.use(generalLimiter);
app.use(inputValidationMiddleware);
app.use(express.static(path.join(__dirname, 'public'))); app.use(express.static(path.join(__dirname, 'public')));
app.use('/public', express.static(path.join(__dirname, 'public'))); app.use('/public', express.static(path.join(__dirname, 'public')));
app.use(express.urlencoded({ extended: true })); app.use(express.urlencoded({ extended: true, limit: '10mb' }));
app.use(bodyParser.json()); app.use(bodyParser.json({ limit: '10mb' }));
app.use(session({ app.use(session({
secret: crypto.randomBytes(64).toString('hex'), secret: crypto.randomBytes(64).toString('hex'),
resave: false, resave: false,
saveUninitialized: true, saveUninitialized: false, // Plus sécurisé
name: 'sessionId', // Nom personnalisé pour ne pas révéler la stack
cookie: { cookie: {
secure: process.env.NODE_ENV === 'production', secure: process.env.NODE_ENV === 'production',
maxAge: 24 * 60 * 60 * 1000 httpOnly: true, // Protection XSS
maxAge: 24 * 60 * 60 * 1000,
sameSite: 'strict' // Protection CSRF
} }
})); }));
app.use(passport.initialize()); app.use(passport.initialize());

View File

@@ -9,7 +9,46 @@
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@10"></script> <script src="https://cdn.jsdelivr.net/npm/sweetalert2@10"></script>
<link rel="stylesheet" href="../public/css/dashboard.styles.css"> <link rel="stylesheet" href="../public/css/dashboard.styles.css">
<link rel="stylesheet" href="../public/css/dropdown-fixes.css">
<style> <style>
/* Changelog Modal Styles */
#changelogModal .modal-content {
border: none;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
border-radius: 15px;
}
#changelogModal .modal-header {
border-top-left-radius: 15px;
border-top-right-radius: 15px;
border-bottom: none;
}
#changelogModal .modal-body ul li {
padding: 8px 0;
transition: all 0.2s ease;
}
#changelogModal .modal-body ul li:hover {
padding-left: 10px;
background-color: rgba(102, 126, 234, 0.05);
border-radius: 5px;
}
#changelogModal .modal-footer {
border-top: 1px solid #e9ecef;
}
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.6;
}
}
body { body {
font-family: 'Inter', sans-serif; font-family: 'Inter', sans-serif;
@@ -29,33 +68,7 @@
</style> </style>
</head> </head>
<body class="animate"> <body class="animate">
<div class="context-menu" style="display: none;">
<button class="menu-item" data-action="open">
<i class="fas fa-folder-open"></i>
<span>Ouvrir</span>
</button>
<button class="menu-item" data-action="rename">
<i class="fas fa-edit"></i>
<span>Renommer</span>
</button>
<button class="menu-item" data-action="collaborate">
<i class="fas fa-users"></i>
<span>Collaborer</span>
</button>
<button class="menu-item" data-action="copy-link">
<i class="fas fa-link"></i>
<span>Copier le lien</span>
</button>
<button class="menu-item" data-action="move">
<i class="fas fa-file-export"></i>
<span>Déplacer</span>
</button>
<div class="menu-separator"></div>
<button class="menu-item destructive" data-action="delete">
<i class="fas fa-trash-alt"></i>
<span>Supprimer</span>
</button>
</div>
<nav class="navbar navbar-expand-md navbar-light bg-light header"> <nav class="navbar navbar-expand-md navbar-light bg-light header">
<div class="container-fluid"> <div class="container-fluid">
<a class="navbar-brand" href="/dpanel/dashboard"> <a class="navbar-brand" href="/dpanel/dashboard">
@@ -69,22 +82,15 @@
<div class="collapse navbar-collapse" id="navbarNav"> <div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ml-auto"> <ul class="navbar-nav ml-auto">
<li class="nav-item"> <li class="nav-item">
<a href="/dpanel/upload" class="btn btn-primary"> <button type="button" class="btn btn-primary" id="uploadToDashboardBtn">
<i class="fas fa-cloud-upload-alt"></i> Téléverser <i class="fas fa-cloud-upload-alt"></i> Téléverser
</a> </button>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<button type="button" class="btn btn-success" id="newFolderBtn"> <button type="button" class="btn btn-success" id="newFolderBtn">
<i class="fas fa-folder-open"></i> Nouveau <i class="fas fa-folder-open"></i> Nouveau
</button> </button>
</li> </li>
<li class="nav-item">
<button id="themeSwitcher" class="btn btn-secondary p-2">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" />
</svg>
</button>
</li>
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<button class="btn dropdown-toggle nav-btn" id="accountDropdownBtn" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <button class="btn dropdown-toggle nav-btn" id="accountDropdownBtn" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<img <img
@@ -111,6 +117,14 @@
</a> </a>
<% } %> <% } %>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a class="dropdown-item" href="#" id="showChangelogBtn">
<span style="display: inline-block; width: 20px; text-align: center;">
<i class="fas fa-rocket"></i>
</span>
Nouveautés v1.2.0-beta
<span class="badge badge-danger ml-2" style="animation: pulse 2s infinite;">Nouveau</span>
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="/auth/logout"> <a class="dropdown-item" href="/auth/logout">
<span style="display: inline-block; width: 20px; text-align: center;"> <span style="display: inline-block; width: 20px; text-align: center;">
<i class="fas fa-sign-out-alt"></i> <i class="fas fa-sign-out-alt"></i>
@@ -128,27 +142,28 @@
<div class="container mt-4 animate"> <div class="container mt-4 animate">
<!-- Menu contextuel --> <!-- Menu contextuel -->
<div id="contextMenu" class="context-menu" style="display: none; position: fixed; z-index: 1000;"> <div id="contextMenu" class="context-menu" style="display: none; position: fixed; z-index: 1000;">
<div class="bg-white rounded-lg shadow-lg py-2 w-48"> <a href="#" class="context-item-open menu-item">
<a href="#" class="context-item-open w-full text-left px-4 py-2 hover:bg-gray-100 flex items-center"> <i class="fas fa-folder-open"></i> <span>Ouvrir</span>
<i class="fas fa-folder-open mr-2"></i> Ouvrir </a>
</a> <button class="context-item-rename menu-item">
<button class="context-item-rename w-full text-left px-4 py-2 hover:bg-gray-100 flex items-center"> <i class="fas fa-edit"></i> <span>Renommer</span>
<i class="fas fa-edit mr-2"></i> Renommer </button>
</button> <button class="context-item-collaborate menu-item">
<button class="context-item-collaborate w-full text-left px-4 py-2 hover:bg-gray-100 flex items-center"> <i class="fas fa-users"></i> <span>Collaborer</span>
<i class="fas fa-users mr-2"></i> Collaborer </button>
</button> <button class="context-item-share menu-item">
<button class="context-item-share w-full text-left px-4 py-2 hover:bg-gray-100 flex items-center"> <i class="fas fa-share-alt"></i> <span>Copier le lien</span>
<i class="fas fa-share-alt mr-2"></i> Copier le lien </button>
</button> <button class="context-item-move menu-item">
<button class="context-item-move w-full text-left px-4 py-2 hover:bg-gray-100 flex items-center"> <i class="fas fa-file-export"></i> <span>Déplacer</span>
<i class="fas fa-file-export mr-2"></i> Déplacer </button>
</button> <button class="context-item-leave menu-item" style="color: #f59e0b;">
<div class="border-t border-gray-200 my-2"></div> <i class="fas fa-sign-out-alt"></i> <span>Quitter ce dossier</span>
<button class="context-item-delete w-full text-left px-4 py-2 hover:bg-gray-100 flex items-center text-red-600"> </button>
<i class="fas fa-trash-alt mr-2"></i> Supprimer <div class="menu-separator"></div>
</button> <button class="context-item-delete menu-item destructive" style="color: #ef4444;">
</div> <i class="fas fa-trash-alt"></i> <span>Supprimer</span>
</button>
</div> </div>
<div class="form-container"> <div class="form-container">
@@ -165,7 +180,6 @@
<th class="text-center">Type</th> <th class="text-center">Type</th>
<th class="text-center">Propriétaire</th> <th class="text-center">Propriétaire</th>
<th class="text-center">Taille</th> <th class="text-center">Taille</th>
<th class="text-right">Actions</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -194,32 +208,6 @@
</span> </span>
</td> </td>
<td class="text-center">-</td> <td class="text-center">-</td>
<td class="text-right">
<div class="dropdown">
<button class="btn btn-link btn-sm dropdown-toggle" type="button" data-bs-toggle="dropdown">
<i class="fas fa-ellipsis-v"></i>
</button>
<div class="dropdown-menu dropdown-menu-end">
<a href="/dpanel/dashboard/folder/<%= encodeURIComponent(folder.name) %>"
class="dropdown-item">
<i class="fas fa-folder-open mr-2"></i> Ouvrir
</a>
<button class="dropdown-item rename-folder-btn" data-folder-name="<%= folder.name %>">
<i class="fas fa-edit mr-2"></i> Renommer
</button>
<button class="dropdown-item text-primary toggle-collaboration-btn"
data-item-name="<%= folder.name %>"
data-item-type="folder"
data-is-collaborative="<%= folder.isCollaborative ? 'true' : 'false' %>">
<i class="fas fa-users mr-2"></i> Collaborer
</button>
<div class="dropdown-divider"></div>
<button class="dropdown-item text-danger delete-folder-btn" data-folder-name="<%= folder.name %>">
<i class="fas fa-trash-alt mr-2"></i> Supprimer
</button>
</div>
</div>
</td>
</tr> </tr>
<% }); %> <% }); %>
@@ -247,22 +235,6 @@
</span> </span>
</td> </td>
<td class="text-center">-</td> <td class="text-center">-</td>
<td class="text-right">
<div class="dropdown">
<button class="btn btn-link btn-sm dropdown-toggle" type="button" data-bs-toggle="dropdown">
<i class="fas fa-ellipsis-v"></i>
</button>
<div class="dropdown-menu dropdown-menu-end">
<a href="/dpanel/dashboard/folder/shared/<%= folder.owner %>/<%= encodeURIComponent(folder.folderName) %>"
class="dropdown-item">
<i class="fas fa-folder-open mr-2"></i> Ouvrir
</a>
<button class="dropdown-item leave-folder-btn">
<i class="fas fa-user-minus mr-2"></i> Quitter
</button>
</div>
</div>
</td>
</tr> </tr>
<% }); %> <% }); %>
<% } %> <% } %>
@@ -290,28 +262,6 @@
<%= file.size %> octets <%= file.size %> octets
</span> </span>
</td> </td>
<td class="text-right">
<div class="dropdown">
<button class="btn btn-link btn-sm dropdown-toggle" type="button" data-bs-toggle="dropdown">
<i class="fas fa-ellipsis-v"></i>
</button>
<div class="dropdown-menu dropdown-menu-end">
<button class="dropdown-item rename-file-btn" data-file-name="<%= file.name %>">
<i class="fas fa-edit mr-2"></i> Renommer
</button>
<button class="dropdown-item copy-button" data-file-url="<%= file.url %>">
<i class="fas fa-copy mr-2"></i> Copier le lien
</button>
<button class="dropdown-item move-file-btn" data-file-name="<%= file.name %>">
<i class="fas fa-file-export mr-2"></i> Déplacer
</button>
<div class="dropdown-divider"></div>
<button class="dropdown-item text-danger delete-file-button" data-file-name="<%= file.name %>">
<i class="fas fa-trash-alt mr-2"></i> Supprimer
</button>
</div>
</div>
</td>
</tr> </tr>
<% }); %> <% }); %>
</tbody> </tbody>
@@ -422,9 +372,351 @@
</div> </div>
</div> </div>
<!-- Changelog Modal -->
<div class="modal fade" id="changelogModal" tabindex="-1" role="dialog" aria-labelledby="changelogModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-lg" role="document">
<div class="modal-content">
<div class="modal-header" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white;">
<h5 class="modal-title" id="changelogModalLabel">
<i class="fas fa-rocket"></i> Nouveautés - Version 1.2.0-beta
</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Fermer" style="color: white; opacity: 1;">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body" style="max-height: 70vh; overflow-y: auto;">
<!-- Nouvelles fonctionnalités -->
<div class="mb-4">
<h6 class="text-primary font-weight-bold mb-3">
<i class="fas fa-star text-warning"></i> Nouvelles Fonctionnalités
</h6>
<ul class="list-unstyled">
<li class="mb-2">
<i class="fas fa-check-circle text-success mr-2"></i>
<strong>Upload direct dans les dossiers</strong> - Téléversez vos fichiers directement dans n'importe quel dossier avec le bouton "Téléverser ici"
</li>
<li class="mb-2">
<i class="fas fa-check-circle text-success mr-2"></i>
<strong>Upload dans dossiers partagés</strong> - Collaborez en uploadant des fichiers dans les dossiers partagés avec vous
</li>
<li class="mb-2">
<i class="fas fa-check-circle text-success mr-2"></i>
<strong>Menu contextuel amélioré</strong> - Clic droit sur fichiers/dossiers pour actions rapides (Renommer, Copier, Déplacer, Supprimer)
</li>
<li class="mb-2">
<i class="fas fa-check-circle text-success mr-2"></i>
<strong>Double-clic pour ouvrir</strong> - Double-cliquez sur un fichier pour l'ouvrir ou un dossier pour naviguer
</li>
<li class="mb-2">
<i class="fas fa-check-circle text-success mr-2"></i>
<strong>Upload modal dans dashboard</strong> - Plus besoin de redirection, uploadez directement depuis le dashboard
</li>
<li class="mb-2">
<i class="fas fa-check-circle text-success mr-2"></i>
<strong>Dark mode synchronisé</strong> - Le thème sombre est maintenant synchronisé sur toutes les pages
</li>
</ul>
</div>
<!-- Améliorations -->
<div class="mb-4">
<h6 class="text-info font-weight-bold mb-3">
<i class="fas fa-magic"></i> Améliorations
</h6>
<ul class="list-unstyled">
<li class="mb-2">
<i class="fas fa-arrow-up text-info mr-2"></i>
Interface simplifiée - Colonne "Actions" supprimée pour plus de clarté
</li>
<li class="mb-2">
<i class="fas fa-arrow-up text-info mr-2"></i>
SweetAlert2 intégré - Toutes les confirmations utilisent maintenant des modales élégantes
</li>
<li class="mb-2">
<i class="fas fa-arrow-up text-info mr-2"></i>
Breadcrumb corrigé - Navigation par fil d'Ariane améliorée
</li>
<li class="mb-2">
<i class="fas fa-arrow-up text-info mr-2"></i>
Upload par chunks optimisé - Meilleure gestion des gros fichiers (jusqu'à 1GB)
</li>
</ul>
</div>
<!-- Corrections de bugs -->
<div class="mb-4">
<h6 class="text-danger font-weight-bold mb-3">
<i class="fas fa-bug"></i> Corrections de Bugs
</h6>
<ul class="list-unstyled">
<li class="mb-2">
<i class="fas fa-wrench text-danger mr-2"></i>
Résolution des erreurs de statistiques manquantes dans l'admin
</li>
<li class="mb-2">
<i class="fas fa-wrench text-danger mr-2"></i>
Correction CORS/COEP pour les avatars DiceBear
</li>
<li class="mb-2">
<i class="fas fa-wrench text-danger mr-2"></i>
Fichier dropdown-fixes.css créé (erreur 404 corrigée)
</li>
<li class="mb-2">
<i class="fas fa-wrench text-danger mr-2"></i>
Erreurs JavaScript dans folder.js corrigées
</li>
<li class="mb-2">
<i class="fas fa-wrench text-danger mr-2"></i>
Violation CSP pour CDN Tailwind corrigée
</li>
<li class="mb-2">
<i class="fas fa-wrench text-danger mr-2"></i>
Variable user manquante dans les routes admin ajoutée
</li>
<li class="mb-2">
<i class="fas fa-wrench text-danger mr-2"></i>
Fonction initForm manquante créée
</li>
</ul>
</div>
<!-- Sécurité -->
<div class="mb-3">
<h6 class="text-warning font-weight-bold mb-3">
<i class="fas fa-shield-alt"></i> Sécurité
</h6>
<ul class="list-unstyled">
<li class="mb-2">
<i class="fas fa-lock text-warning mr-2"></i>
Vérification des permissions pour dossiers partagés
</li>
<li class="mb-2">
<i class="fas fa-lock text-warning mr-2"></i>
Génération automatique de noms de fichiers sécurisés
</li>
<li class="mb-2">
<i class="fas fa-lock text-warning mr-2"></i>
Headers de sécurité optimisés
</li>
</ul>
</div>
<hr>
<div class="text-center text-muted small">
<p class="mb-1">Merci d'utiliser CDN-APP-INSIDER !</p>
<p class="mb-0">
<i class="fas fa-heart text-danger"></i>
Développé par <strong>Dinawo - Group Myaxrin Labs</strong>
</p>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-dismiss="modal">Fermer</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="uploadModal" tabindex="-1" role="dialog" aria-labelledby="uploadModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Téléverser dans le dossier racine</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Fermer">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<form id="dashboardUploadForm">
<div class="form-group">
<label for="uploadFileInput">Sélectionnez un fichier :</label>
<input type="file" class="form-control" id="uploadFileInput" name="file" required>
<small class="form-text text-muted">Taille maximale : 1 GB</small>
</div>
<div class="form-group">
<label for="uploadExpiryDate">Date d'expiration (optionnel) :</label>
<input type="date" class="form-control" id="uploadExpiryDate" name="expiryDate">
</div>
<div class="form-group">
<label for="uploadPassword">Mot de passe (optionnel) :</label>
<input type="password" class="form-control" id="uploadPassword" name="password" placeholder="Au moins 6 caractères">
</div>
<div class="progress" id="uploadProgress" style="display: none;">
<div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 0%">
<span class="sr-only">0% Complete</span>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Annuler</button>
<button type="button" class="btn btn-primary" id="confirmUpload">
<i class="fas fa-upload"></i> Téléverser
</button>
</div>
</div>
</div>
</div>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"></script> <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"></script>
<script src="/public/js/dashboard.js"></script> <script src="/public/js/dashboard.js"></script>
<script>
// Upload functionality
document.addEventListener('DOMContentLoaded', function() {
// Afficher le changelog uniquement si l'utilisateur ne l'a pas encore vu pour cette version
const changelogVersion = '1.2.0-beta';
const seenChangelog = localStorage.getItem('changelog_seen_' + changelogVersion);
if (!seenChangelog) {
// Délai de 1 seconde pour laisser la page se charger
setTimeout(function() {
$('#changelogModal').modal('show');
}, 1000);
// Marquer comme vu quand la modal est fermée
$('#changelogModal').on('hidden.bs.modal', function() {
localStorage.setItem('changelog_seen_' + changelogVersion, 'true');
});
}
// Bouton pour voir le changelog manuellement
const showChangelogBtn = document.getElementById('showChangelogBtn');
if (showChangelogBtn) {
showChangelogBtn.addEventListener('click', function(e) {
e.preventDefault();
$('#changelogModal').modal('show');
});
}
document.getElementById('uploadToDashboardBtn').addEventListener('click', function() {
$('#uploadModal').modal('show');
});
const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB chunks
async function generateSecurityCode() {
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
let code = '';
for (let i = 0; i < 6; i++) {
code += characters.charAt(Math.floor(Math.random() * characters.length));
}
return code;
}
async function formatSecureFileName(originalFileName) {
const now = new Date();
const date = now.toISOString().slice(0,10).replace(/-/g, '');
const securityCode = await generateSecurityCode();
const lastDot = originalFileName.lastIndexOf('.');
const fileName = lastDot !== -1 ? originalFileName.substring(0, lastDot) : originalFileName;
const fileExt = lastDot !== -1 ? originalFileName.substring(lastDot) : '';
return `${date}_${securityCode}_${fileName}${fileExt}`;
}
document.getElementById('confirmUpload').addEventListener('click', async function() {
const fileInput = document.getElementById('uploadFileInput');
const file = fileInput.files[0];
if (!file) {
Swal.fire({
icon: 'error',
title: 'Erreur',
text: 'Veuillez sélectionner un fichier'
});
return;
}
const MAX_FILE_SIZE = 1024 * 1024 * 1024; // 1GB
if (file.size > MAX_FILE_SIZE) {
Swal.fire({
icon: 'error',
title: 'Fichier trop volumineux',
text: 'La taille maximale est de 1 GB'
});
return;
}
const password = document.getElementById('uploadPassword').value;
if (password && password.length < 6) {
Swal.fire({
icon: 'error',
title: 'Erreur',
text: 'Le mot de passe doit contenir au moins 6 caractères'
});
return;
}
const expiryDate = document.getElementById('uploadExpiryDate').value;
const secureFileName = await formatSecureFileName(file.name);
const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
let uploadedChunks = 0;
const progressBar = document.querySelector('#uploadProgress .progress-bar');
document.getElementById('uploadProgress').style.display = 'block';
document.getElementById('confirmUpload').disabled = true;
try {
for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) {
const start = chunkIndex * CHUNK_SIZE;
const end = Math.min(start + CHUNK_SIZE, file.size);
const chunk = file.slice(start, end);
const formData = new FormData();
formData.append('file', chunk);
formData.append('chunkIndex', chunkIndex);
formData.append('totalChunks', totalChunks);
formData.append('filename', secureFileName);
formData.append('originalFilename', file.name);
formData.append('targetFolder', ''); // Dossier racine
if (expiryDate) formData.append('expiryDate', expiryDate);
if (password) formData.append('password', password);
const response = await fetch('/api/dpanel/upload', {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error(`Upload failed: ${response.statusText}`);
}
uploadedChunks++;
const progress = (uploadedChunks / totalChunks) * 100;
progressBar.style.width = progress + '%';
progressBar.textContent = Math.round(progress) + '%';
}
Swal.fire({
icon: 'success',
title: 'Fichier téléversé !',
text: 'Le fichier a été téléversé avec succès'
}).then(() => {
location.reload();
});
$('#uploadModal').modal('hide');
} catch (error) {
console.error('Upload error:', error);
Swal.fire({
icon: 'error',
title: 'Erreur',
text: 'Une erreur est survenue lors du téléversement'
});
} finally {
document.getElementById('confirmUpload').disabled = false;
document.getElementById('uploadProgress').style.display = 'none';
progressBar.style.width = '0%';
}
});
});
</script>
</body> </body>
</html> </html>

View File

@@ -1,201 +1,807 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="fr">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" <title>Dashboard CDN</title>
integrity="sha384-GLhlTQ8iRABdZLl6O5oVMWSktQOp6b7In1Zl3/JiR3eZB1+nHN/8u8UqXj2l1tji" crossorigin="anonymous">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
<script src="https://code.jquery.com/jquery-3.6.4.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css">
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script src="https://cdn.jsdelivr.net/npm/toastify-js"></script>
<link rel="stylesheet" href="/public/css/styles.css" />
<script src="/public/js/folder.js"></script>
<title>Dashboard</title>
<link rel="icon" href="/public/assets/homelab_logo.png" /> <link rel="icon" href="/public/assets/homelab_logo.png" />
<link href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@10"></script>
<link rel="stylesheet" href="/public/css/dashboard.styles.css">
<link rel="stylesheet" href="/public/css/dropdown-fixes.css">
<style>
body {
font-family: 'Inter', sans-serif;
background-color: hsl(var(--background));
color: hsl(var(--foreground));
transition: background-color 0.3s ease, color 0.3s ease;
background-image: url('<%= user.wallpaper %>');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
background-attachment: fixed;
margin: 0;
min-height: 100vh;
display: flex;
flex-direction: column;
}
</style>
</head> </head>
<body class="animate">
<style> <nav class="navbar navbar-expand-md navbar-light bg-light header">
body { <div class="container-fluid">
background-image: url('<%= user.wallpaper %>'); <a class="navbar-brand" href="/dpanel/dashboard">
background-size: cover; Dashboard CDN
background-position: center; <span class="bg-purple-100 text-purple-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded-full dark:bg-purple-900 dark:text-purple-300">Beta</span>
background-repeat: no-repeat; </a>
background-attachment: fixed; <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav"
margin: 0; aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
height: 100vh; <span class="navbar-toggler-icon"></span>
overflow: hidden; </button>
} <div class="collapse navbar-collapse" id="navbarNav">
</style> <ul class="navbar-nav ml-auto">
<li class="nav-item">
<button type="button" class="btn btn-primary" id="uploadToFolderBtn">
<body class="light-mode"> <i class="fas fa-cloud-upload-alt"></i> Téléverser ici
<nav class="navbar navbar-expand-lg navbar-light bg-light header">
<a class="navbar-brand">
Dashboard CDN
<span class="badge badge-info ml-1">Beta</span>
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav"
aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse ml-auto" id="navbarNav">
<ul class="navbar-nav ml-auto">
<li class="nav-item">
<button class="btn btn-warning btn-round mr-2 animated-button" onclick="window.location.href='/dpanel/dashboard';">
<i class="fas fa-home"></i>Page principal</button>
</li>
<li class="nav-item">
<form action="/dpanel/upload" class="form-inline">
<button class="btn btn-primary btn-round mr-2 animated-button">
<i class="fas fa-cloud-upload-alt"></i> Téléverser un fichier
</button> </button>
</form> </li>
</li> <li class="nav-item">
<li class="nav-item"> <button type="button" class="btn btn-success" id="newFolderBtn">
<button id="styleSwitcher" class="btn btn-link btn-round animated-button"> <i class="fas fa-folder-open"></i> Nouveau
<span id="themeIcon" class="fas theme-icon"></span> </button>
</button> </li>
</li> <li class="nav-item dropdown">
</ul> <button class="btn dropdown-toggle nav-btn" id="accountDropdownBtn" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<img
src="<%= user.profilePicture ? user.profilePicture : `https://api.dicebear.com/7.x/initials/svg?seed=${encodeURIComponent(user.name)}&background=%234e54c8&radius=50` %>"
alt="<%= user.name %>"
class="rounded-full user-avatar"
style="width: 40px; height: 40px;"
onerror="this.src=`https://api.dicebear.com/7.x/initials/svg?seed=${encodeURIComponent('<%= user.name %>')}&background=%234e54c8&radius=50`"
/>
</button>
<div class="dropdown-menu dropdown-menu-right" id="accountDropdownMenu">
<a class="dropdown-item" href="/dpanel/dashboard/profil">
<span style="display: inline-block; width: 20px; text-align: center;">
<i class="fas fa-user"></i>
</span>
Mon profil
</a>
<% if (user.role === 'admin') { %>
<a class="dropdown-item" href="/dpanel/dashboard/admin">
<span style="display: inline-block; width: 20px; text-align: center;">
<i class="fas fa-user-shield"></i>
</span>
Administration du site
</a>
<% } %>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="/dpanel/dashboard">
<span style="display: inline-block; width: 20px; text-align: center;">
<i class="fas fa-rocket"></i>
</span>
Nouveautés v1.2.0-beta
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="/auth/logout">
<span style="display: inline-block; width: 20px; text-align: center;">
<i class="fas fa-sign-out-alt"></i>
</span>
Déconnexion
</a>
</div>
</li>
</ul>
</div>
</div> </div>
</nav> </nav>
<div class="alert alert-primary text-center" role="alert">
La refonte est presque terminée ! Il ne reste qu'une seule page à finaliser. Myaxrin Labs s'excuse pour le délai et travaille activement à l'amélioration de l'application pour vous offrir une meilleure expérience très prochainement.
<div class="container mt-4 animate">
<!-- Menu contextuel -->
<div id="contextMenu" class="context-menu" style="display: none; position: fixed; z-index: 1000;">
<a href="#" class="context-item-open menu-item">
<i class="fas fa-folder-open"></i> <span>Ouvrir</span>
</a>
<button class="context-item-rename menu-item">
<i class="fas fa-edit"></i> <span>Renommer</span>
</button>
<button class="context-item-share menu-item">
<i class="fas fa-share-alt"></i> <span>Copier le lien</span>
</button>
<button class="context-item-move menu-item">
<i class="fas fa-file-export"></i> <span>Déplacer</span>
</button>
<div class="menu-separator"></div>
<button class="context-item-delete menu-item destructive" style="color: #ef4444;">
<i class="fas fa-trash-alt"></i> <span>Supprimer</span>
</button>
</div>
<!-- Breadcrumb -->
<nav aria-label="breadcrumb" class="mb-3">
<ol class="breadcrumb custom-breadcrumb">
<li class="breadcrumb-item"><a href="/dpanel/dashboard">Accueil</a></li>
<% if (currentFolder) { %>
<% let pathSegments = currentFolder.split('/').filter(s => s); %>
<% pathSegments.forEach((segment, index) => { %>
<% let pathSoFar = pathSegments.slice(0, index + 1).join('/'); %>
<li class="breadcrumb-item <%= (index === pathSegments.length - 1) ? 'active' : '' %>">
<% if (index === pathSegments.length - 1) { %>
<%= segment %>
<% } else { %>
<a href="/dpanel/dashboard/folder/<%= pathSoFar %>"><%= segment %></a>
<% } %>
</li>
<% }); %>
<% } %>
</ol>
</nav>
<div class="form-container">
<div class="flex justify-between items-center mb-4">
<input type="text" id="searchInput" class="form-control w-1/2" placeholder="Rechercher par nom de fichier">
<button id="searchButton" class="btn btn-primary">Rechercher</button>
</div>
<div class="table-responsive">
<table class="table" id="fileTable">
<thead>
<tr>
<th>Nom</th>
<th class="text-center">Type</th>
<th class="text-center">Taille</th>
</tr>
</thead>
<tbody>
<% files.forEach(file => { %>
<tr data-type="<%= file.type %>" data-name="<%= file.name %>" data-url="<%= file.type === 'folder' ? '/dpanel/dashboard/folder/' + currentFolder + '/' + encodeURIComponent(file.name) : file.url %>" class="hover:bg-gray-50" style="cursor: pointer;">
<td>
<div class="d-flex align-items-center">
<% if (file.type === 'folder') { %>
<i class="fas fa-folder text-warning mr-2"></i>
<%= file.name %>
<% } else { %>
<i class="fas fa-file text-secondary mr-2"></i>
<%= file.name %>
<% } %>
</div>
</td>
<td class="text-center">
<% if (file.type === 'folder') { %>
<span class="badge badge-warning">Dossier</span>
<% } else { %>
<span class="badge badge-secondary">Fichier</span>
<% } %>
</td>
<td class="text-center">
<% if (file.type === 'folder') { %>
<%
function calculateFolderSize(contents) {
let totalSize = 0;
if (contents && Array.isArray(contents)) {
contents.forEach(item => {
if (item.type === 'file' && item.size) {
totalSize += item.size;
} else if (item.type === 'folder' && item.contents) {
totalSize += calculateFolderSize(item.contents);
}
});
}
return totalSize;
}
const folderSize = calculateFolderSize(file.contents);
%>
<span class="file-size" data-size="<%= folderSize %>">
<%= folderSize %> octets
</span>
<% } else { %>
<span class="file-size" data-size="<%= file.size %>">
<%= file.size %> octets
</span>
<% } %>
</td>
</tr>
<% }); %>
</tbody>
</table>
</div>
</div>
</div> </div>
<nav aria-label="breadcrumb">
<ol class="breadcrumb custom-breadcrumb">
<li class="breadcrumb-item"><a href="/dpanel/dashboard">Accueil</a></li>
<% let pathSegments = currentFolder.split('/'); %>
<% pathSegments.forEach((segment, index) => { %>
<% let pathSoFar = pathSegments.slice(0, index + 1).join('/'); %>
<li class="breadcrumb-item <%= (index === pathSegments.length - 1) ? 'active' : '' %>">
<% if (index === pathSegments.length - 1) { %>
<%= segment %>
<% } else { %>
<a href="/dpanel/dashboard/folder/<%= pathSoFar %>"><%= segment %></a>
<% } %>
</li>
<% }); %>
</ol>
</nav>
</body>
<% function formatSize(sizeInBytes) { <footer class="footer mt-auto py-3 bg-light">
if (sizeInBytes < 1024) { <div class="container">
return `${sizeInBytes} octets`; <span class="text-muted">Version: <span id="version-number">...</span> | &copy; <span id="current-year"></span> Myaxrin Labs</span>
} else if (sizeInBytes < 1024 * 1024) { <a href="#" class="float-right" onclick="displayMetadata()">Metadata</a>
return `${(sizeInBytes / 1024).toFixed(2)} Ko`; </div>
} else if (sizeInBytes < 1024 * 1024 * 1024) { </footer>
return `${(sizeInBytes / (1024 * 1024)).toFixed(2)} Mo`;
} else {
return `${(sizeInBytes / (1024 * 1024 * 1024)).toFixed(2)} Go`;
}
}
%>
<div class="container mt-4 table-container"> <script>
<div class="table-responsive"> document.getElementById('current-year').textContent = new Date().getFullYear();
<table class="table w-100"> </script>
<thead>
<tr> <div class="modal fade" id="metadataModal" tabindex="-1" role="dialog" aria-labelledby="metadataModalLabel" aria-hidden="true">
<th>Nom du fichier</th> <div class="modal-dialog modal-dialog-centered" role="document">
<th>Taille</th> <div class="modal-content">
<th class="text-right">Action</th> <div class="modal-header">
</tr> <h5 class="modal-title" id="metadataModalLabel">Metadata</h5>
</thead> <button type="button" class="close" data-dismiss="modal" aria-label="Close">
<tbody> <span aria-hidden="true">&times;</span>
<% files.forEach(file => { %> </button>
<tr data-extension="<%= file.extension %>" data-type="<%= file.type %>"> </div>
<% if (fileInfoNames.includes(file.name)) { %> <div class="modal-body">
<td><a href="#" onclick="showFileInfo('<%= file.name %>')"><%= file.name %></a></td> <p><i class="fas fa-code-branch"></i> Version de Build: <span id="buildVersion"></span></p>
<% } else { %> <p><i class="fab fa-node"></i> Version de Node.js: <span id="nodeVersion"></span></p>
<td><%= file.name %></td> <p><i class="fas fa-server"></i> Version de Express.js: <span id="expressVersion"></span></p>
<% } %> <p><i class="fas fa-hashtag"></i> SHA de Build: <span id="buildSha"></span></p>
<td> <p><i class="fas fa-windows"></i> Type d'OS: <span id="osType"></span></p>
<% if (file.type === 'folder') { %> <p><i class="fas fa-laptop-code"></i> Version d'OS: <span id="osRelease"></span></p>
<% const folderSize = calculateFolderSize(file.contents); %> </div>
<%= (folderSize !== undefined && !isNaN(folderSize)) ? formatSize(folderSize) : 'Taille inconnue' %> <div class="modal-footer">
<% } else { %> <button type="button" class="btn btn-secondary" data-dismiss="modal">Fermer</button>
<% </div>
const fileSizeInBytes = file.size; </div>
let fileSize; </div>
if (fileSizeInBytes !== undefined && !isNaN(fileSizeInBytes) && fileSizeInBytes >= 0) {
fileSize = formatSize(fileSizeInBytes);
} else {
console.error('Invalid file size:', fileSizeInBytes);
fileSize = 'Taille inconnue';
}
%>
<%= fileSize %>
<% } %>
</td>
<td class="d-flex justify-content-end align-items-center">
<% if (file.type === 'folder') { %>
<a href="/dpanel/dashboard/folder/<%= file.name %>" class="btn btn-primary btn-round mb-2">
<i class="fas fa-folder-open fa-xs btn-icon animated-button "></i> Accéder
</a>
<% } else { %>
<button class="btn btn-primary btn-round animated-button" onclick="renameFile('<%= folderName %>', '<%= file.name %>')">
<i class="fas fa-edit fa-xs btn-icon"></i> Renommer
</button>
<form class="file-actions mb-2" id="deleteForm" action="/api/dpanel/dashboard/delete" method="post">
<input type="hidden" name="_method" value="DELETE">
<input type="hidden" name="filename" value="<%= file.name %>">
<button class="delete-button btn btn-danger btn-round animated-button" type="button" onclick="confirmDeleteFile('<%= currentFolder %>', '<%= file.name %>')">
<i class="fas fa-trash-alt fa-xs btn-icon"></i>
</button>
</form>
<form class="file-actions mb-2">
<div class="copy-link-container d-flex align-items-center">
<input type="text" class="file-link form-control rounded mr-2" value="<%= file.url %>" readonly style="display: none;">
<button class="button copy-button btn btn-success btn-round animated-button" data-file="<%= file.name %>">
<i class="fas fa-copy fa-xs btn-icon"></i>
</button>
</div>
</form>
<form id="moveFileForm" class="file-actions d-flex align-items-center mb-2">
<input type="hidden" name="fileName" value="<%= file.name %>">
<input type="hidden" name="userName" value="<%= userName %>">
<input type="hidden" name="oldFolderName" value="<%= folderName %>">
<select class="form-control rounded mr-2 custom-dropdown" name="newFolderName">
<option value="" disabled selected>Déplacer vers...</option>
<option value="root">Dossier Racine</option>
<% allFolders.forEach(folder => { %>
<option value="<%= folder %>"><%= folder %></option>
<% }); %>
</select>
<button type="submit" class="btn btn-success btn-round animated-button">Déplacer</button>
</form>
<% } %>
</td>
</tr>
<% }); %>
</tbody>
</table>
</div>
</div> </div>
<div class="modal fade" id="moveFileModal" tabindex="-1" role="dialog" aria-labelledby="moveFileModalLabel" aria-hidden="true">
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"></script> <div class="modal-dialog modal-dialog-centered" role="document">
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"></script> <div class="modal-content">
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"></script> <div class="modal-header">
<h5 class="modal-title">Déplacer le fichier</h5>
<script> <button type="button" class="close" data-dismiss="modal" aria-label="Fermer">
<span aria-hidden="true">&times;</span>
</script> </button>
</div>
<div class="container"> <div class="modal-body">
<footer class="py-3 my-4"> <form id="moveFileForm">
<ul class="nav justify-content-center border-bottom pb-3 mb-3"> <input type="hidden" id="moveFileName" name="fileName">
<li class="nav-item"><a class="nav-link px-2 text-muted">Version: <span id="version-number">...</span></a></li> <input type="hidden" id="moveUserName" name="userName" value="<%= userName %>">
</ul> <input type="hidden" id="moveOldFolderName" name="oldFolderName" value="<%= folderName %>">
<p class="text-center text-muted">&copy; 2024 Myaxrin Labs</p> <select class="form-control" id="moveFolderSelect" name="newFolderName">
</footer> <option value="" disabled selected>Choisir un dossier...</option>
<option value="root">Dossier Racine</option>
<% allFolders.forEach(folder => { %>
<option value="<%= folder %>"><%= folder %></option>
<% }); %>
</select>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Annuler</button>
<button type="button" class="btn btn-primary" id="confirmMoveFile">Déplacer</button>
</div>
</div>
</div> </div>
</body> </div>
<div class="modal fade" id="uploadModal" tabindex="-1" role="dialog" aria-labelledby="uploadModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Téléverser dans <%= currentFolder || 'ce dossier' %></h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Fermer">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<form id="folderUploadForm">
<div class="form-group">
<label for="uploadFileInput">Sélectionnez un fichier :</label>
<input type="file" class="form-control" id="uploadFileInput" name="file" required>
<small class="form-text text-muted">Taille maximale : 1 GB</small>
</div>
<div class="form-group">
<label for="uploadExpiryDate">Date d'expiration (optionnel) :</label>
<input type="date" class="form-control" id="uploadExpiryDate" name="expiryDate">
</div>
<div class="form-group">
<label for="uploadPassword">Mot de passe (optionnel) :</label>
<input type="password" class="form-control" id="uploadPassword" name="password" placeholder="Au moins 6 caractères">
</div>
<div class="progress" id="uploadProgress" style="display: none;">
<div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 0%">
<span class="sr-only">0% Complete</span>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Annuler</button>
<button type="button" class="btn btn-primary" id="confirmUpload">
<i class="fas fa-upload"></i> Téléverser
</button>
</div>
</div>
</div>
</div>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"></script>
<script src="/public/js/folder.js"></script>
<script>
// Fonction pour formater la taille des fichiers
function formatFileSize(bytes) {
if (bytes === 0) return '0 octets';
const k = 1024;
const sizes = ['octets', 'Ko', 'Mo', 'Go', 'To'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
// Formater toutes les tailles de fichiers au chargement
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.file-size').forEach(function(element) {
const size = parseInt(element.getAttribute('data-size'));
if (!isNaN(size)) {
element.textContent = formatFileSize(size);
}
});
// Gestion du menu contextuel
const contextMenu = document.getElementById('contextMenu');
let currentContextRow = null;
// Clic droit sur les lignes du tableau
document.querySelectorAll('#fileTable tbody tr').forEach(function(row) {
row.addEventListener('contextmenu', function(e) {
e.preventDefault();
currentContextRow = row;
const type = row.getAttribute('data-type');
const name = row.getAttribute('data-name');
const url = row.getAttribute('data-url');
// Positionner le menu
contextMenu.style.display = 'block';
contextMenu.style.left = e.pageX + 'px';
contextMenu.style.top = e.pageY + 'px';
// Configuration selon le type et les permissions
const openItem = contextMenu.querySelector('.context-item-open');
const moveItem = contextMenu.querySelector('.context-item-move');
const renameItem = contextMenu.querySelector('.context-item-rename');
const shareItem = contextMenu.querySelector('.context-item-share');
const deleteItem = contextMenu.querySelector('.context-item-delete');
const separator = contextMenu.querySelector('.menu-separator');
// Récupérer les données de collaboration depuis le serveur
// isCollaborativeFolder fait référence au dossier ACTUEL (celui dans lequel on navigue)
const isCollaborativeFolder = <%= typeof isCollaborativeFolder !== 'undefined' && isCollaborativeFolder ? 'true' : 'false' %>;
const isOwner = <%= typeof isOwner !== 'undefined' && isOwner ? 'true' : 'false' %>;
// Masquer tous les éléments par défaut
openItem.style.display = 'none';
moveItem.style.display = 'none';
renameItem.style.display = 'none';
shareItem.style.display = 'none';
deleteItem.style.display = 'none';
if (separator) separator.style.display = 'none';
if (type === 'folder') {
// Pour les sous-dossiers : uniquement "Ouvrir"
openItem.style.display = 'block';
openItem.href = url;
} else {
// Pour les fichiers
// Permissions basées sur si on est dans un dossier partagé
if (isCollaborativeFolder && !isOwner) {
// Invité dans un dossier partagé : peut seulement copier le lien
shareItem.style.display = 'block';
} else {
// Propriétaire ou dossier non partagé : tous les droits
renameItem.style.display = 'block';
shareItem.style.display = 'block';
moveItem.style.display = 'block';
if (separator) separator.style.display = 'block';
deleteItem.style.display = 'block';
}
}
});
});
// Fermer le menu contextuel
document.addEventListener('click', function() {
contextMenu.style.display = 'none';
});
// Renommer
contextMenu.querySelector('.context-item-rename').addEventListener('click', async function() {
if (currentContextRow) {
const name = currentContextRow.getAttribute('data-name');
const type = currentContextRow.getAttribute('data-type');
if (type === 'file') {
const result = await Swal.fire({
title: 'Renommer le fichier',
input: 'text',
inputLabel: 'Nouveau nom',
inputValue: name,
showCancelButton: true,
confirmButtonText: 'Renommer',
cancelButtonText: 'Annuler',
inputValidator: (value) => {
if (!value) {
return 'Vous devez entrer un nom !';
}
}
});
if (result.isConfirmed && result.value !== name) {
const newName = result.value;
// Appel API pour renommer
fetch('/api/dpanel/dashboard/rename', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
folderName: '<%= folderName %>',
oldName: name,
newName: newName
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
Swal.fire({
icon: 'success',
title: 'Fichier renommé !',
text: 'Le fichier a été renommé avec succès'
}).then(() => location.reload());
} else {
Swal.fire({
icon: 'error',
title: 'Erreur',
text: data.message || 'Une erreur est survenue'
});
}
})
.catch(error => {
Swal.fire({
icon: 'error',
title: 'Erreur',
text: 'Une erreur est survenue lors du renommage'
});
});
}
}
}
contextMenu.style.display = 'none';
});
// Copier le lien
contextMenu.querySelector('.context-item-share').addEventListener('click', function() {
if (currentContextRow) {
const url = currentContextRow.getAttribute('data-url');
navigator.clipboard.writeText(url).then(function() {
Swal.fire({
icon: 'success',
title: 'Lien copié !',
text: 'Le lien a été copié dans le presse-papiers',
toast: true,
position: 'top-end',
showConfirmButton: false,
timer: 3000
});
});
}
contextMenu.style.display = 'none';
});
// Déplacer
contextMenu.querySelector('.context-item-move').addEventListener('click', function() {
if (currentContextRow) {
const name = currentContextRow.getAttribute('data-name');
document.getElementById('moveFileName').value = name;
$('#moveFileModal').modal('show');
}
contextMenu.style.display = 'none';
});
// Supprimer
contextMenu.querySelector('.context-item-delete').addEventListener('click', async function() {
if (currentContextRow) {
const name = currentContextRow.getAttribute('data-name');
const type = currentContextRow.getAttribute('data-type');
if (type === 'file') {
const result = await Swal.fire({
title: 'Êtes-vous sûr ?',
text: 'Cette action est irréversible !',
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#d33',
cancelButtonColor: '#3085d6',
confirmButtonText: 'Oui, supprimer !',
cancelButtonText: 'Annuler'
});
if (result.isConfirmed) {
fetch('/api/dpanel/dashboard/delete', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
filename: name
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
Swal.fire({
icon: 'success',
title: 'Fichier supprimé !',
text: 'Le fichier a été supprimé avec succès'
}).then(() => location.reload());
} else {
Swal.fire({
icon: 'error',
title: 'Erreur',
text: data.message || 'Une erreur est survenue'
});
}
})
.catch(error => {
Swal.fire({
icon: 'error',
title: 'Erreur',
text: 'Une erreur est survenue lors de la suppression'
});
});
}
}
}
contextMenu.style.display = 'none';
});
// Confirmation du déplacement
document.getElementById('confirmMoveFile').addEventListener('click', function() {
const fileName = document.getElementById('moveFileName').value;
const newFolderName = document.getElementById('moveFolderSelect').value;
const userName = document.getElementById('moveUserName').value;
const oldFolderName = document.getElementById('moveOldFolderName').value;
if (!newFolderName) {
Swal.fire({
icon: 'error',
title: 'Erreur',
text: 'Veuillez sélectionner un dossier de destination'
});
return;
}
// Envoi de la requête de déplacement
fetch('/api/dpanel/dashboard/move', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
fileName: fileName,
userName: userName,
oldFolderName: oldFolderName,
newFolderName: newFolderName
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
Swal.fire({
icon: 'success',
title: 'Fichier déplacé !',
text: 'Le fichier a été déplacé avec succès'
}).then(() => {
location.reload();
});
} else {
Swal.fire({
icon: 'error',
title: 'Erreur',
text: data.message || 'Une erreur est survenue'
});
}
})
.catch(error => {
Swal.fire({
icon: 'error',
title: 'Erreur',
text: 'Une erreur est survenue lors du déplacement'
});
});
$('#moveFileModal').modal('hide');
});
// Recherche
document.getElementById('searchButton').addEventListener('click', function() {
const searchValue = document.getElementById('searchInput').value.toLowerCase();
const rows = document.querySelectorAll('#fileTable tbody tr');
rows.forEach(function(row) {
const name = row.getAttribute('data-name').toLowerCase();
if (name.includes(searchValue)) {
row.style.display = '';
} else {
row.style.display = 'none';
}
});
});
// Recherche en temps réel
document.getElementById('searchInput').addEventListener('keyup', function() {
const searchValue = this.value.toLowerCase();
const rows = document.querySelectorAll('#fileTable tbody tr');
rows.forEach(function(row) {
const name = row.getAttribute('data-name').toLowerCase();
if (name.includes(searchValue)) {
row.style.display = '';
} else {
row.style.display = 'none';
}
});
});
// Dark mode synchronisé
const savedTheme = localStorage.getItem('theme') || 'light';
if (savedTheme === 'dark') {
document.documentElement.classList.add('dark');
document.body.classList.add('dark-mode');
} else {
document.documentElement.classList.remove('dark');
document.body.classList.remove('dark-mode');
}
// Double-clic pour ouvrir
document.querySelectorAll('#fileTable tbody tr').forEach(function(row) {
row.addEventListener('dblclick', function() {
const url = row.getAttribute('data-url');
const type = row.getAttribute('data-type');
if (type === 'folder') {
window.location.href = url;
} else {
window.open(url, '_blank');
}
});
});
// Gestion de l'upload dans le dossier
document.getElementById('uploadToFolderBtn').addEventListener('click', function() {
$('#uploadModal').modal('show');
});
const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB chunks
async function generateSecurityCode() {
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
let code = '';
for (let i = 0; i < 6; i++) {
code += characters.charAt(Math.floor(Math.random() * characters.length));
}
return code;
}
async function formatSecureFileName(originalFileName) {
const now = new Date();
const date = now.toISOString().slice(0,10).replace(/-/g, '');
const securityCode = await generateSecurityCode();
const lastDot = originalFileName.lastIndexOf('.');
const fileName = lastDot !== -1 ? originalFileName.substring(0, lastDot) : originalFileName;
const fileExt = lastDot !== -1 ? originalFileName.substring(lastDot) : '';
return `${date}_${securityCode}_${fileName}${fileExt}`;
}
document.getElementById('confirmUpload').addEventListener('click', async function() {
const fileInput = document.getElementById('uploadFileInput');
const file = fileInput.files[0];
if (!file) {
Swal.fire({
icon: 'error',
title: 'Erreur',
text: 'Veuillez sélectionner un fichier'
});
return;
}
const MAX_FILE_SIZE = 1024 * 1024 * 1024; // 1GB
if (file.size > MAX_FILE_SIZE) {
Swal.fire({
icon: 'error',
title: 'Fichier trop volumineux',
text: 'La taille maximale est de 1 GB'
});
return;
}
const password = document.getElementById('uploadPassword').value;
if (password && password.length < 6) {
Swal.fire({
icon: 'error',
title: 'Erreur',
text: 'Le mot de passe doit contenir au moins 6 caractères'
});
return;
}
const expiryDate = document.getElementById('uploadExpiryDate').value;
const currentFolder = '<%= currentFolder %>';
const isSharedFolder = <%= typeof isSharedFolder !== 'undefined' && isSharedFolder ? 'true' : 'false' %>;
const ownerName = '<%= typeof ownerName !== "undefined" ? ownerName : "" %>';
const secureFileName = await formatSecureFileName(file.name);
const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
let uploadedChunks = 0;
const progressBar = document.querySelector('#uploadProgress .progress-bar');
document.getElementById('uploadProgress').style.display = 'block';
document.getElementById('confirmUpload').disabled = true;
try {
for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) {
const start = chunkIndex * CHUNK_SIZE;
const end = Math.min(start + CHUNK_SIZE, file.size);
const chunk = file.slice(start, end);
const formData = new FormData();
formData.append('file', chunk);
formData.append('chunkIndex', chunkIndex);
formData.append('totalChunks', totalChunks);
formData.append('filename', secureFileName);
formData.append('originalFilename', file.name);
formData.append('targetFolder', currentFolder);
// Pour les dossiers partagés
if (isSharedFolder && ownerName) {
formData.append('isSharedFolder', 'true');
formData.append('ownerName', ownerName);
}
if (expiryDate) formData.append('expiryDate', expiryDate);
if (password) formData.append('password', password);
const response = await fetch('/api/dpanel/upload', {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error(`Upload failed: ${response.statusText}`);
}
uploadedChunks++;
const progress = (uploadedChunks / totalChunks) * 100;
progressBar.style.width = progress + '%';
progressBar.textContent = Math.round(progress) + '%';
}
Swal.fire({
icon: 'success',
title: 'Fichier téléversé !',
text: 'Le fichier a été téléversé avec succès'
}).then(() => {
location.reload();
});
$('#uploadModal').modal('hide');
} catch (error) {
console.error('Upload error:', error);
Swal.fire({
icon: 'error',
title: 'Erreur',
text: 'Une erreur est survenue lors du téléversement'
});
} finally {
document.getElementById('confirmUpload').disabled = false;
document.getElementById('uploadProgress').style.display = 'none';
progressBar.style.width = '0%';
}
});
});
</script>
</body>
</html> </html>

View File

@@ -3,24 +3,12 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Paramètres Admin</title> <title>Panneau d'Administration - CDN Insider</title>
<link rel="icon" href="/public/assets/homelab_logo.png" /> <link rel="icon" href="/public/assets/homelab_logo.png" />
<link href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@10"></script> <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<style> <style>
body { @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
background-image: url('<%= user.wallpaper %>'); /* Placeholder for dynamic background */
background-size: cover;
background-position: center;
background-repeat: no-repeat;
background-attachment: fixed;
margin: 0;
height: 100vh;
overflow: hidden;
}
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap');
:root { :root {
--background: 0 0% 100%; --background: 0 0% 100%;
@@ -67,170 +55,702 @@
--ring: 212.7 26.8% 83.9%; --ring: 212.7 26.8% 83.9%;
} }
* {
box-sizing: border-box;
}
body { body {
font-family: 'Inter', sans-serif; font-family: 'Inter', sans-serif;
background-color: hsl(var(--background)); background-color: hsl(var(--background));
color: hsl(var(--foreground)); color: hsl(var(--foreground));
transition: background-color 0.3s ease, color 0.3s ease; transition: background-color 0.3s ease, color 0.3s ease;
margin: 0;
padding: 0;
min-height: 100vh;
background-image: url('<%= user.wallpaper %>');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
background-attachment: fixed;
}
.backdrop {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.3);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
z-index: 1;
} }
.container { .container {
max-width: 960px; position: relative;
z-index: 2;
max-width: 1200px;
margin: 0 auto; margin: 0 auto;
padding: 2rem; padding: 2rem;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
} }
.form-container { .profile-card {
background-color: hsl(var(--card)); background: hsl(var(--card));
border: 1px solid hsl(var(--border)); border: 1px solid hsl(var(--border));
border-radius: var(--radius); border-radius: 16px;
padding: 2rem; box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25), 0 0 0 1px rgba(255, 255, 255, 0.05);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
overflow: hidden;
width: 100%;
max-width: 900px;
animation: slideIn 0.5s ease-out;
} }
.form-group { @keyframes slideIn {
from {
opacity: 0;
transform: translateY(20px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
/* Header admin */
.profile-header {
padding: 2rem;
text-align: center;
background: linear-gradient(135deg, #dc2626 0%, #991b1b 100%);
color: white;
position: relative;
overflow: hidden;
}
.profile-header::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><circle cx="20" cy="20" r="2" fill="white" opacity="0.1"/><circle cx="80" cy="80" r="2" fill="white" opacity="0.1"/><circle cx="40" cy="60" r="1" fill="white" opacity="0.05"/></svg>');
opacity: 0.3;
}
.admin-icon {
position: relative;
z-index: 2;
margin-bottom: 1rem; margin-bottom: 1rem;
} }
.form-control { .admin-icon i {
width: 100%; font-size: 4rem;
padding: 0.5rem; color: rgba(255, 255, 255, 0.9);
filter: drop-shadow(0 10px 30px rgba(0, 0, 0, 0.3));
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
}
.user-info {
position: relative;
z-index: 2;
}
.user-name {
font-size: 2rem;
font-weight: 700;
margin-bottom: 0.5rem;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.user-subtitle {
font-size: 1rem;
opacity: 0.9;
margin-top: 0.5rem;
}
.role-badge {
display: inline-block;
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
padding: 0.5rem 1rem;
border-radius: 25px;
font-size: 0.85rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
box-shadow: 0 4px 12px rgba(245, 158, 11, 0.4);
margin-top: 1rem;
}
/* Onglets */
.tabs-container {
background: hsl(var(--card));
border-bottom: 1px solid hsl(var(--border));
}
.tabs {
display: flex;
overflow-x: auto;
scrollbar-width: none;
-ms-overflow-style: none;
}
.tabs::-webkit-scrollbar {
display: none;
}
.tab {
padding: 1.25rem 2rem;
cursor: pointer;
transition: all 0.3s ease;
white-space: nowrap;
display: flex;
align-items: center;
gap: 0.5rem;
background: transparent;
color: hsl(var(--muted-foreground));
border: none;
font-size: 0.95rem;
font-weight: 500;
position: relative;
border-bottom: 3px solid transparent;
}
.tab:hover {
background-color: hsl(var(--accent));
color: hsl(var(--accent-foreground));
}
.tab.active {
background: hsl(var(--primary));
color: hsl(var(--primary-foreground));
border-bottom-color: hsl(var(--primary));
font-weight: 600;
}
.tab.active::after {
content: '';
position: absolute;
bottom: -3px;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg, hsl(var(--primary)), hsl(var(--ring)));
border-radius: 3px 3px 0 0;
}
.tab i {
font-size: 1.1rem;
}
/* Contenu des onglets */
.tab-content {
display: none;
padding: 2rem;
animation: fadeIn 0.3s ease-out;
}
.tab-content.active {
display: block;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
/* Stats cards */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.stat-card {
background: hsl(var(--card));
border: 1px solid hsl(var(--border)); border: 1px solid hsl(var(--border));
border-radius: var(--radius); border-radius: 12px;
background-color: hsl(var(--background)); padding: 1.5rem;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.stat-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, #3b82f6, #8b5cf6);
}
.stat-card:hover {
transform: translateY(-4px);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
}
.stat-icon {
font-size: 2rem;
margin-bottom: 1rem;
color: hsl(var(--primary));
}
.stat-label {
font-size: 0.85rem;
color: hsl(var(--muted-foreground));
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.stat-value {
font-size: 1.75rem;
font-weight: 700;
color: hsl(var(--foreground));
margin-top: 0.5rem;
}
/* Actions rapides */
.quick-actions {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-bottom: 2rem;
}
.quick-action {
background: hsl(var(--card));
border: 1px solid hsl(var(--border));
border-radius: 12px;
padding: 1.5rem;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
text-decoration: none;
color: hsl(var(--foreground));
position: relative;
overflow: hidden;
}
.quick-action::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent);
transition: left 0.5s;
}
.quick-action:hover::before {
left: 100%;
}
.quick-action:hover {
transform: translateY(-4px);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
text-decoration: none;
color: hsl(var(--foreground)); color: hsl(var(--foreground));
} }
.quick-action-icon {
font-size: 2.5rem;
margin-bottom: 1rem;
color: hsl(var(--primary));
transition: transform 0.3s ease;
}
.quick-action:hover .quick-action-icon {
transform: scale(1.1);
}
.quick-action-title {
font-weight: 600;
margin-bottom: 0.5rem;
font-size: 1rem;
}
.quick-action-desc {
font-size: 0.85rem;
color: hsl(var(--muted-foreground));
}
/* System info */
.system-info-grid {
display: grid;
gap: 1rem;
}
.system-info-item {
background: hsl(var(--card));
border: 1px solid hsl(var(--border));
border-radius: 8px;
padding: 1rem 1.5rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.system-info-label {
font-weight: 500;
color: hsl(var(--muted-foreground));
}
.system-info-value {
font-weight: 600;
color: hsl(var(--foreground));
}
/* Boutons */
.btn { .btn {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
border-radius: var(--radius); gap: 0.5rem;
padding: 0.875rem 1.5rem;
border-radius: 25px;
font-weight: 500; font-weight: 500;
font-size: 0.95rem;
transition: all 0.3s ease; transition: all 0.3s ease;
cursor: pointer; cursor: pointer;
width: 100%; /* Full width */ border: none;
padding: 0.75rem; /* Adjust padding as needed */ text-decoration: none;
position: relative;
overflow: hidden;
}
.btn::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
transition: left 0.5s;
}
.btn:hover::before {
left: 100%;
}
.btn:active {
transform: scale(0.98);
} }
.btn-primary { .btn-primary {
background-color: hsl(var(--primary)); background: linear-gradient(135deg, hsl(var(--primary)) 0%, hsl(var(--ring)) 100%);
color: hsl(var(--primary-foreground)); color: hsl(var(--primary-foreground));
box-shadow: 0 4px 14px 0 rgba(0, 0, 0, 0.1);
} }
.btn-primary:hover { .btn-primary:hover {
opacity: 0.9; transform: translateY(-2px);
box-shadow: 0 8px 25px 0 rgba(0, 0, 0, 0.15);
} }
.btn-secondary { .btn-secondary {
background-color: hsl(var(--secondary)); background: hsl(var(--secondary));
color: hsl(var(--secondary-foreground)); color: hsl(var(--secondary-foreground));
border: 1px solid hsl(var(--border));
} }
.btn-secondary:hover { .btn-secondary:hover {
opacity: 0.9; background: hsl(var(--accent));
color: hsl(var(--accent-foreground));
transform: translateY(-1px);
} }
#themeSwitcher { .btn-full {
width: 100%;
}
/* Theme switcher */
.theme-switcher {
position: fixed; position: fixed;
top: 1rem; top: 2rem;
right: 1rem; right: 2rem;
z-index: 1000; /* Ensure it stays above other elements */ z-index: 10;
background-color: hsl(var(--secondary)); background: hsl(var(--card));
border: none;
border-radius: 50%;
padding: 0.5rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: background-color 0.3s ease;
}
#themeSwitcher:hover {
background-color: hsl(var(--primary));
}
#themeSwitcher svg {
width: 24px;
height: 24px;
color: hsl(var(--primary-foreground));
}
.animate {
animation: fadeIn 0.5s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.icon-spacing {
margin-right: 8px;
}
.swal2-toast {
background-color: hsl(var(--card));
color: hsl(var(--foreground));
border: 1px solid hsl(var(--border)); border: 1px solid hsl(var(--border));
border-radius: var(--radius); border-radius: 50%;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); padding: 0.75rem;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.1);
}
.theme-switcher:hover {
transform: scale(1.1) rotate(180deg);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
}
.theme-switcher svg {
width: 1.25rem;
height: 1.25rem;
color: hsl(var(--foreground));
}
/* Responsive */
@media (max-width: 768px) {
.container {
padding: 1rem;
}
.profile-header {
padding: 1.5rem;
}
.user-name {
font-size: 1.5rem;
}
.tab {
padding: 1rem 1.5rem;
font-size: 0.9rem;
}
.tab-content {
padding: 1.5rem;
}
.theme-switcher {
top: 1rem;
right: 1rem;
}
.stats-grid {
grid-template-columns: 1fr;
}
.quick-actions {
grid-template-columns: 1fr;
}
}
/* Toast notifications */
.swal2-toast {
background-color: hsl(var(--card)) !important;
color: hsl(var(--foreground)) !important;
border: 1px solid hsl(var(--border)) !important;
border-radius: var(--radius) !important;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1) !important;
} }
.dark .swal2-toast { .dark .swal2-toast {
background-color: #000; /* Fond noir en mode sombre */ background-color: hsl(var(--card)) !important;
color: #fff; /* Texte blanc */ color: hsl(var(--foreground)) !important;
border: 1px solid #333; /* Bordure grise foncée */ border: 1px solid hsl(var(--border)) !important;
} }
</style> </style>
</head> </head>
<body class="animate dark"> <body class="dark">
<button id="themeSwitcher"> <div class="backdrop"></div>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" />
</svg>
</button>
<div class="min-h-screen flex items-center justify-center p-4"> <div class="container">
<div class="container mt-8"> <div class="profile-card">
<h1 class="text-3xl font-semibold mb-6 text-center">Paramètres Admin</h1> <!-- Header admin -->
<div class="profile-header">
<div class="admin-icon">
<i class="fas fa-user-shield"></i>
</div>
<div class="user-info">
<h1 class="user-name">Panneau d'Administration</h1>
<p class="user-subtitle">Centre de contrôle du système CDN Insider</p>
<div class="role-badge">
<i class="fas fa-crown"></i> Administrateur
</div>
</div>
</div>
<div class="form-container"> <!-- Onglets -->
<div class="flex flex-col gap-4"> <div class="tabs-container">
<a href="/dpanel/dashboard/admin/users?page=1&limit=10" class="btn btn-primary flex items-center justify-center space-x-2"> <div class="tabs">
<i class="fas fa-users icon-spacing"></i> <button class="tab active" data-tab="overview">
<span>Gérer les utilisateurs</span> <i class="fas fa-chart-pie"></i>
</a> Aperçu
<a href="/dpanel/dashboard/admin/settingsetup" class="btn btn-primary flex items-center justify-center space-x-2">
<i class="fas fa-cogs icon-spacing"></i>
<span>Modifier les paramètres de configuration</span>
</a>
<a href="/dpanel/dashboard/admin/stats-logs" class="btn btn-primary flex items-center justify-center space-x-2">
<i class="fas fa-chart-bar icon-spacing"></i>
<span>Afficher les statistiques & logs</span>
</a>
<a href="/dpanel/dashboard/admin/Privacy-Security" class="btn btn-primary flex items-center justify-center space-x-2">
<i class="fas fa-shield-alt icon-spacing"></i>
<span>Confidentialité & Sécurité</span>
</a>
<button onclick="window.location.href='/dpanel/dashboard';" class="btn btn-secondary w-full py-2 mt-4">
<i class="fas fa-arrow-left icon-spacing"></i>
Retour au Dashboard
</button> </button>
<button class="tab" data-tab="management">
<i class="fas fa-tasks"></i>
Gestion
</button>
<button class="tab" data-tab="system">
<i class="fas fa-server"></i>
Système
</button>
</div>
</div>
<!-- Contenu de l'onglet Aperçu -->
<div class="tab-content active" id="overview">
<div class="stats-grid">
<div class="stat-card">
<div class="stat-icon">
<i class="fas fa-users"></i>
</div>
<div class="stat-label">Utilisateurs totaux</div>
<div class="stat-value"><%= users.length %></div>
</div>
<div class="stat-card">
<div class="stat-icon">
<i class="fas fa-folder"></i>
</div>
<div class="stat-label">Dossiers actifs</div>
<div class="stat-value"><%= stats.foldersCount %></div>
</div>
<div class="stat-card">
<div class="stat-icon">
<i class="fas fa-hdd"></i>
</div>
<div class="stat-label">Espace utilisé</div>
<div class="stat-value">
<%
const sizeInGB = (stats.totalSize / (1024 * 1024 * 1024)).toFixed(2);
const sizeInMB = (stats.totalSize / (1024 * 1024)).toFixed(2);
%>
<%= stats.totalSize > 1073741824 ? sizeInGB + ' GB' : sizeInMB + ' MB' %>
</div>
</div>
</div>
<div style="text-align: center; margin-top: 2rem;">
<a href="/dpanel/dashboard" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i>
Retour au Dashboard
</a>
</div>
</div>
<!-- Contenu de l'onglet Gestion -->
<div class="tab-content" id="management">
<div class="quick-actions">
<a href="/dpanel/dashboard/admin/users?page=1&limit=10" class="quick-action">
<div class="quick-action-icon">
<i class="fas fa-users-cog"></i>
</div>
<div class="quick-action-title">Utilisateurs</div>
<div class="quick-action-desc">Gérer les comptes utilisateurs</div>
</a>
<a href="/dpanel/dashboard/admin/settingsetup" class="quick-action">
<div class="quick-action-icon">
<i class="fas fa-cogs"></i>
</div>
<div class="quick-action-title">Configuration</div>
<div class="quick-action-desc">Paramètres du système</div>
</a>
<a href="/dpanel/dashboard/admin/stats-logs" class="quick-action">
<div class="quick-action-icon">
<i class="fas fa-chart-line"></i>
</div>
<div class="quick-action-title">Stats & Logs</div>
<div class="quick-action-desc">Statistiques et journaux</div>
</a>
<a href="/dpanel/dashboard/admin/Privacy-Security" class="quick-action">
<div class="quick-action-icon">
<i class="fas fa-shield-alt"></i>
</div>
<div class="quick-action-title">Sécurité</div>
<div class="quick-action-desc">Confidentialité et sécurité</div>
</a>
</div>
</div>
<!-- Contenu de l'onglet Système -->
<div class="tab-content" id="system">
<div class="system-info-grid">
<div class="system-info-item">
<span class="system-info-label">
<i class="fas fa-server"></i> Statut du serveur
</span>
<span class="system-info-value" style="color: #10b981;">
<i class="fas fa-circle"></i> En ligne
</span>
</div>
<div class="system-info-item">
<span class="system-info-label">
<i class="fas fa-code-branch"></i> Version
</span>
<span class="system-info-value">v1.2.0-beta</span>
</div>
<div class="system-info-item">
<span class="system-info-label">
<i class="fas fa-clock"></i> Uptime
</span>
<span class="system-info-value">
<%
const days = Math.floor(stats.uptime / 86400);
const hours = Math.floor((stats.uptime % 86400) / 3600);
const minutes = Math.floor((stats.uptime % 3600) / 60);
const uptimeText = days > 0 ? `${days}j ${hours}h ${minutes}m` :
hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`;
%>
<%= uptimeText %>
</span>
</div>
<div class="system-info-item">
<span class="system-info-label">
<i class="fas fa-database"></i> Base de données
</span>
<span class="system-info-value" style="color: #10b981;">
<i class="fas fa-check-circle"></i> Connectée
</span>
</div>
<div class="system-info-item">
<span class="system-info-label">
<i class="fas fa-memory"></i> Utilisation mémoire
</span>
<span class="system-info-value"><%= stats.memoryUsage %> MB</span>
</div>
<div class="system-info-item">
<span class="system-info-label">
<i class="fas fa-microchip"></i> Charge CPU
</span>
<span class="system-info-value"><%= stats.cpuUsage %> ms</span>
</div>
</div>
<div style="margin-top: 2rem; padding: 1.5rem; background: hsl(var(--muted)); border-radius: 12px; border-left: 4px solid #3b82f6;">
<div style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.75rem;">
<i class="fas fa-info-circle" style="color: #3b82f6; font-size: 1.25rem;"></i>
<strong style="color: hsl(var(--foreground));">Informations système</strong>
</div>
<p style="margin: 0; font-size: 0.9rem; color: hsl(var(--muted-foreground)); line-height: 1.6;">
Le système CDN Insider fonctionne de manière optimale. Toutes les connexions sont sécurisées et les services sont opérationnels.
Surveillez régulièrement les statistiques pour maintenir les performances du système.
</p>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Theme Switcher -->
<button class="theme-switcher" id="themeSwitcher">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" />
</svg>
</button>
<script> <script>
// =================== VARIABLES GLOBALES ===================
const body = document.body; const body = document.body;
const themeSwitcher = document.getElementById('themeSwitcher'); const themeSwitcher = document.getElementById('themeSwitcher');
// =================== GESTION DU THÈME ===================
function setTheme(theme) { function setTheme(theme) {
if (theme === 'dark') { if (theme === 'dark') {
body.classList.add('dark'); body.classList.add('dark');
@@ -240,6 +760,7 @@
localStorage.setItem('theme', theme); localStorage.setItem('theme', theme);
} }
// Initialisation du thème
const savedTheme = localStorage.getItem('theme'); const savedTheme = localStorage.getItem('theme');
if (savedTheme) { if (savedTheme) {
setTheme(savedTheme); setTheme(savedTheme);
@@ -255,12 +776,38 @@
} }
}); });
// =================== GESTION DES ONGLETS ===================
function switchTab(tabName) {
// Désactiver tous les onglets
document.querySelectorAll('.tab').forEach(tab => {
tab.classList.remove('active');
});
// Masquer tout le contenu
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.remove('active');
});
// Activer l'onglet et le contenu sélectionnés
document.querySelector(`[data-tab="${tabName}"]`).classList.add('active');
document.getElementById(tabName).classList.add('active');
}
// Gestionnaires d'événements pour les onglets
document.querySelectorAll('.tab').forEach(tab => {
tab.addEventListener('click', function() {
const tabName = this.getAttribute('data-tab');
switchTab(tabName);
});
});
// =================== NOTIFICATIONS ===================
function showToast(icon, title) { function showToast(icon, title) {
Swal.fire({ Swal.fire({
icon: icon, icon: icon,
title: title, title: title,
showConfirmButton: false, showConfirmButton: false,
timer: 1800, timer: 3000,
toast: true, toast: true,
position: 'top-end', position: 'top-end',
customClass: { customClass: {
@@ -268,6 +815,18 @@
} }
}); });
} }
// =================== INITIALISATION ===================
document.addEventListener('DOMContentLoaded', function() {
// Animation d'entrée
setTimeout(() => {
document.querySelector('.profile-card').style.opacity = '1';
document.querySelector('.profile-card').style.transform = 'translateY(0) scale(1)';
}, 100);
console.log('🎨 Panneau d\'administration chargé !');
console.log('✨ Interface moderne activée');
});
</script> </script>
</body> </body>
</html> </html>

View File

@@ -3,25 +3,12 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Confidentialité et Sécurité</title> <title>Confidentialité & Sécurité - Interface Admin</title>
<link rel="icon" href="/public/assets/homelab_logo.png" /> <link rel="icon" href="/public/assets/homelab_logo.png" />
<link href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@10"></script> <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style> <style>
body { @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
background-image: url('<%= user.wallpaper %>');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
background-attachment: fixed;
margin: 0;
height: 100vh;
overflow: hidden;
}
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap');
:root { :root {
--background: 0 0% 100%; --background: 0 0% 100%;
@@ -68,153 +55,541 @@
--ring: 212.7 26.8% 83.9%; --ring: 212.7 26.8% 83.9%;
} }
* {
box-sizing: border-box;
}
body { body {
font-family: 'Inter', sans-serif; font-family: 'Inter', sans-serif;
background-color: hsl(var(--background)); background-color: hsl(var(--background));
color: hsl(var(--foreground)); color: hsl(var(--foreground));
transition: background-color 0.3s ease, color 0.3s ease; transition: background-color 0.3s ease, color 0.3s ease;
margin: 0;
padding: 0;
min-height: 100vh;
background-image: url('<%= user.wallpaper %>');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
background-attachment: fixed;
}
.backdrop {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.3);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
z-index: 1;
} }
.container { .container {
max-width: 960px; position: relative;
z-index: 2;
max-width: 1200px;
margin: 0 auto; margin: 0 auto;
padding: 2rem; padding: 2rem;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.admin-card {
background: hsl(var(--card));
border: 1px solid hsl(var(--border));
border-radius: 16px;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25), 0 0 0 1px rgba(255, 255, 255, 0.05);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
overflow: hidden;
width: 100%;
max-width: 1000px;
animation: slideIn 0.5s ease-out;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(20px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.admin-header {
padding: 2rem;
text-align: center;
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
color: white;
position: relative;
overflow: hidden;
}
.admin-header::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><circle cx="20" cy="20" r="2" fill="white" opacity="0.1"/><circle cx="80" cy="80" r="2" fill="white" opacity="0.1"/><circle cx="40" cy="60" r="1" fill="white" opacity="0.05"/></svg>');
opacity: 0.3;
}
.admin-header h1 {
font-size: 2rem;
font-weight: 700;
margin: 0;
position: relative;
z-index: 2;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.admin-content {
padding: 2rem;
} }
.form-container { .section-title {
background-color: hsl(var(--card)); font-size: 1.5rem;
font-weight: 600;
color: hsl(var(--foreground));
margin-bottom: 1.5rem;
display: flex;
align-items: center;
gap: 0.75rem;
}
.section-title i {
color: #10b981;
}
.info-banner {
background: linear-gradient(135deg, rgba(16, 185, 129, 0.1) 0%, rgba(5, 150, 105, 0.1) 100%);
border: 1px solid rgba(16, 185, 129, 0.3);
border-radius: 12px;
padding: 1.5rem;
margin-bottom: 2rem;
display: flex;
align-items: start;
gap: 1rem;
}
.info-banner-icon {
font-size: 1.5rem;
color: #10b981;
flex-shrink: 0;
}
.info-banner-content h3 {
margin: 0 0 0.5rem 0;
font-size: 1.1rem;
font-weight: 600;
color: hsl(var(--foreground));
}
.info-banner-content p {
margin: 0;
font-size: 0.9rem;
color: hsl(var(--muted-foreground));
line-height: 1.6;
}
.quick-actions {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-bottom: 2rem;
}
.quick-action {
background: hsl(var(--card));
border: 1px solid hsl(var(--border)); border: 1px solid hsl(var(--border));
border-radius: var(--radius); border-radius: 12px;
padding: 2rem; padding: 1.5rem;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
text-decoration: none;
color: hsl(var(--foreground));
position: relative;
overflow: hidden;
}
.quick-action::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, #10b981, #059669);
opacity: 0;
transition: opacity 0.3s ease;
}
.quick-action:hover::before {
opacity: 1;
}
.quick-action:hover {
transform: translateY(-4px);
box-shadow: 0 10px 25px rgba(16, 185, 129, 0.2);
text-decoration: none;
color: hsl(var(--foreground));
}
.quick-action-icon {
font-size: 2rem;
margin-bottom: 1rem;
color: #10b981;
}
.quick-action-title {
font-weight: 600;
margin-bottom: 0.5rem;
}
.quick-action-desc {
font-size: 0.85rem;
color: hsl(var(--muted-foreground));
} }
.btn { .btn {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
border-radius: var(--radius); gap: 0.5rem;
padding: 0.875rem 1.5rem;
border-radius: 25px;
font-weight: 500; font-weight: 500;
font-size: 0.95rem;
transition: all 0.3s ease; transition: all 0.3s ease;
cursor: pointer; cursor: pointer;
border: none;
text-decoration: none;
position: relative;
overflow: hidden;
}
.btn::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
transition: left 0.5s;
}
.btn:hover::before {
left: 100%;
}
.btn:active {
transform: scale(0.98);
} }
.btn-primary { .btn-primary {
background-color: hsl(var(--primary)); background: linear-gradient(135deg, #10b981 0%, #059669 100%);
color: hsl(var(--primary-foreground)); color: white;
box-shadow: 0 4px 14px 0 rgba(16, 185, 129, 0.3);
} }
.btn-primary:hover { .btn-primary:hover {
opacity: 0.9; transform: translateY(-2px);
box-shadow: 0 8px 25px 0 rgba(16, 185, 129, 0.4);
} }
.btn-secondary { .btn-secondary {
background-color: hsl(var(--secondary)); background: hsl(var(--secondary));
color: hsl(var(--secondary-foreground)); color: hsl(var(--secondary-foreground));
border: 1px solid hsl(var(--border));
} }
.btn-secondary:hover { .btn-secondary:hover {
opacity: 0.9; background: hsl(var(--accent));
color: hsl(var(--accent-foreground));
transform: translateY(-1px);
} }
#themeSwitcher { .btn-full {
width: 100%;
}
.theme-switcher {
position: fixed; position: fixed;
top: 1rem; top: 2rem;
right: 1rem; right: 2rem;
z-index: 10;
background: hsl(var(--card));
border: 1px solid hsl(var(--border));
border-radius: 50%;
padding: 0.75rem;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.1);
} }
.animate { .theme-switcher:hover {
animation: fadeIn 0.5s ease-out; transform: scale(1.1);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
} }
@keyframes fadeIn { .theme-switcher svg {
from { opacity: 0; } width: 1.25rem;
to { opacity: 1; } height: 1.25rem;
color: hsl(var(--foreground));
} }
.modal { .modal {
display: none; display: none;
position: fixed; position: fixed;
z-index: 1; z-index: 1000;
left: 0; left: 0;
top: 0; top: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
overflow: auto; overflow: auto;
background-color: rgba(0,0,0,0.4); background-color: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(4px);
} }
.modal-content { .modal-content {
background-color: hsl(var(--background)); background: hsl(var(--card));
margin: 15% auto; margin: 5% auto;
padding: 20px; padding: 0;
border: 1px solid hsl(var(--border)); border: 1px solid hsl(var(--border));
width: 80%; width: 90%;
border-radius: var(--radius); max-width: 900px;
border-radius: 16px;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
animation: slideIn 0.3s ease-out;
}
.modal-header {
padding: 1.5rem 2rem;
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
color: white;
border-radius: 16px 16px 0 0;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h2 {
margin: 0;
font-size: 1.5rem;
font-weight: 600;
} }
.close { .close {
color: hsl(var(--muted-foreground)); color: white;
float: right; font-size: 2rem;
font-size: 28px; font-weight: 300;
font-weight: bold; cursor: pointer;
transition: all 0.2s ease;
line-height: 1;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
} }
.close:hover, .close:hover {
.close:focus { background: rgba(255, 255, 255, 0.2);
transform: rotate(90deg);
}
.modal-body {
padding: 2rem;
max-height: 70vh;
overflow-y: auto;
}
.modal-body pre {
background: hsl(var(--muted));
border: 1px solid hsl(var(--border));
border-radius: 8px;
padding: 1.5rem;
overflow-x: auto;
font-size: 0.875rem;
line-height: 1.6;
color: hsl(var(--foreground)); color: hsl(var(--foreground));
text-decoration: none; font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
cursor: pointer; white-space: pre-wrap;
word-wrap: break-word;
}
.empty-state {
text-align: center;
padding: 3rem 2rem;
color: hsl(var(--muted-foreground));
}
.empty-state-icon {
font-size: 4rem;
margin-bottom: 1.5rem;
opacity: 0.3;
color: #10b981;
}
.empty-state h3 {
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 0.5rem;
color: hsl(var(--foreground));
}
.empty-state p {
font-size: 0.95rem;
color: hsl(var(--muted-foreground));
}
@media (max-width: 768px) {
.container {
padding: 1rem;
}
.admin-header {
padding: 1.5rem;
}
.admin-header h1 {
font-size: 1.5rem;
}
.admin-content {
padding: 1.5rem;
}
.theme-switcher {
top: 1rem;
right: 1rem;
}
.modal-content {
width: 95%;
margin: 10% auto;
}
.quick-actions {
grid-template-columns: 1fr;
}
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
} }
</style> </style>
</head> </head>
<body class="animate"> <body class="dark">
<div id="app" class="min-h-screen"> <div class="backdrop"></div>
<div class="container mt-8">
<h1 class="text-3xl font-semibold mb-6 text-center animate">Confidentialité et Sécurité</h1>
<div class="form-container"> <div class="container">
<h2 class="text-2xl font-semibold mb-4">Données d'analyse</h2> <div class="admin-card">
<div class="admin-header">
<h1><i class="fas fa-shield-alt"></i> Confidentialité & Sécurité</h1>
</div>
<div class="admin-content">
<!-- Banner d'information -->
<div class="info-banner">
<div class="info-banner-icon">
<i class="fas fa-info-circle"></i>
</div>
<div class="info-banner-content">
<h3>Centre de Confidentialité</h3>
<p>
Consultez les rapports de confidentialité et de sécurité de votre système.
Ces données sont collectées de manière anonyme et permettent d'améliorer la sécurité de l'application.
</p>
</div>
</div>
<!-- Rapports -->
<div class="section-title">
<i class="fas fa-file-shield"></i>
Rapports de Sécurité
</div>
<% if (reports && reports.length > 0) { %> <% if (reports && reports.length > 0) { %>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4"> <div class="quick-actions">
<% reports.forEach((report, index) => { %> <% reports.forEach((report, index) => { %>
<% if (report) { %> <% if (report) { %>
<div class="text-center"> <div class="quick-action" data-report-index="<%= index %>">
<button class="btn btn-primary w-full reportName" data-index="<%= index %>"><%= report.name %></button> <div class="quick-action-icon">
</div> <i class="fas fa-file-contract"></i>
<div id="myReportModal<%= index %>" class="modal">
<div class="modal-content">
<span class="close" data-index="<%= index %>">&times;</span>
<pre class="whitespace-pre-wrap"><%= report.content %></pre>
</div> </div>
<div class="quick-action-title"><%= report.name %></div>
<div class="quick-action-desc">Cliquer pour voir le détail</div>
</div> </div>
<% } %> <% } %>
<% }); %> <% }); %>
</div> </div>
<% } else { %> <% } else { %>
<p class="text-center text-lg text-gray-500">Aucun rapport disponible pour le moment.</p> <div class="empty-state">
<div class="empty-state-icon">
<i class="fas fa-folder-open"></i>
</div>
<h3>Aucun rapport disponible</h3>
<p>Les rapports de sécurité apparaîtront ici lorsqu'ils seront générés par le système.</p>
</div>
<% } %> <% } %>
<div class="text-center"> <!-- Bouton retour -->
<br><button onclick="window.location.href='/dpanel/dashboard/admin';" class="btn btn-secondary w-full py-2 mt-4"> <div style="margin-top: 2rem;">
<i class="fas fa-arrow-left icon-spacing"></i> <a href="/dpanel/dashboard/admin/" class="btn btn-secondary btn-full">
Retour au Dashboard Admin <i class="fas fa-arrow-left"></i>
</button> Retourner au dashboard admin
</a>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<button id="themeSwitcher" class="btn btn-secondary p-2"> <!-- Modals pour les rapports -->
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6"> <% if (reports && reports.length > 0) { %>
<% reports.forEach((report, index) => { %>
<% if (report) { %>
<div id="reportModal<%= index %>" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2><i class="fas fa-file-contract"></i> <%= report.name %></h2>
<span class="close" data-modal-index="<%= index %>">&times;</span>
</div>
<div class="modal-body">
<pre><%= report.content %></pre>
</div>
</div>
</div>
<% } %>
<% }); %>
<% } %>
<!-- Theme Switcher -->
<button class="theme-switcher" id="themeSwitcher">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" />
</svg> </svg>
</button> </button>
<script> <script>
// =================== VARIABLES GLOBALES ===================
const body = document.body; const body = document.body;
const themeSwitcher = document.getElementById('themeSwitcher'); const themeSwitcher = document.getElementById('themeSwitcher');
// =================== GESTION DU THÈME ===================
function setTheme(theme) { function setTheme(theme) {
if (theme === 'dark') { if (theme === 'dark') {
body.classList.add('dark'); body.classList.add('dark');
@@ -239,30 +614,45 @@
} }
}); });
var reportNames = document.getElementsByClassName("reportName"); // =================== GESTION DES MODALS ===================
var reportCloseButtons = document.getElementsByClassName("close"); const reportButtons = document.querySelectorAll('.quick-action[data-report-index]');
const closeButtons = document.querySelectorAll('.close[data-modal-index]');
for (var i = 0; i < reportNames.length; i++) { reportButtons.forEach(button => {
reportNames[i].addEventListener("click", function(event) { button.addEventListener('click', function() {
var index = event.target.getAttribute("data-index"); const index = this.getAttribute('data-report-index');
var modal = document.getElementById("myReportModal" + index); const modal = document.getElementById('reportModal' + index);
modal.style.display = "block"; if (modal) {
modal.style.display = 'block';
}
}); });
} });
for (var i = 0; i < reportCloseButtons.length; i++) { closeButtons.forEach(button => {
reportCloseButtons[i].addEventListener("click", function(event) { button.addEventListener('click', function() {
var index = event.target.getAttribute("data-index"); const index = this.getAttribute('data-modal-index');
var modal = document.getElementById("myReportModal" + index); const modal = document.getElementById('reportModal' + index);
modal.style.display = "none"; if (modal) {
modal.style.display = 'none';
}
}); });
} });
window.onclick = function(event) { window.onclick = function(event) {
if (event.target.className === "modal") { if (event.target.classList.contains('modal')) {
event.target.style.display = "none"; event.target.style.display = 'none';
} }
} };
// =================== ANIMATIONS ===================
document.addEventListener('DOMContentLoaded', function() {
setTimeout(() => {
document.querySelector('.admin-card').style.opacity = '1';
document.querySelector('.admin-card').style.transform = 'translateY(0) scale(1)';
}, 100);
console.log('🔒 Interface Confidentialité & Sécurité chargée !');
});
</script> </script>
</body> </body>
</html> </html>

View File

@@ -1,27 +1,15 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="fr">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Statistiques du serveur</title> <title>Statistiques & Logs - Interface Admin</title>
<link rel="icon" href="/public/assets/homelab_logo.png" /> <link rel="icon" href="/public/assets/homelab_logo.png" />
<link href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@10"></script> <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style> <style>
body { @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
background-image: url('<%= user.wallpaper %>');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
background-attachment: fixed;
margin: 0;
height: 100vh;
overflow: hidden;
}
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap');
:root { :root {
--background: 0 0% 100%; --background: 0 0% 100%;
@@ -68,172 +56,556 @@
--ring: 212.7 26.8% 83.9%; --ring: 212.7 26.8% 83.9%;
} }
* {
box-sizing: border-box;
}
body { body {
font-family: 'Inter', sans-serif; font-family: 'Inter', sans-serif;
background-color: hsl(var(--background)); background-color: hsl(var(--background));
color: hsl(var(--foreground)); color: hsl(var(--foreground));
transition: background-color 0.3s ease, color 0.3s ease; transition: background-color 0.3s ease, color 0.3s ease;
margin: 0;
padding: 0;
min-height: 100vh;
background-image: url('<%= user.wallpaper %>');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
background-attachment: fixed;
}
.backdrop {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.3);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
z-index: 1;
} }
.container { .container {
max-width: 1250px; position: relative;
z-index: 2;
max-width: 1200px;
margin: 0 auto; margin: 0 auto;
padding: 2rem; padding: 2rem;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.admin-card {
background: hsl(var(--card));
border: 1px solid hsl(var(--border));
border-radius: 16px;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25), 0 0 0 1px rgba(255, 255, 255, 0.05);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
overflow: hidden;
width: 100%;
max-width: 1000px;
animation: slideIn 0.5s ease-out;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(20px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.admin-header {
padding: 2rem;
text-align: center;
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
color: white;
position: relative;
overflow: hidden;
}
.admin-header::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><circle cx="20" cy="20" r="2" fill="white" opacity="0.1"/><circle cx="80" cy="80" r="2" fill="white" opacity="0.1"/><circle cx="40" cy="60" r="1" fill="white" opacity="0.05"/></svg>');
opacity: 0.3;
}
.admin-header h1 {
font-size: 2rem;
font-weight: 700;
margin: 0;
position: relative;
z-index: 2;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.admin-content {
padding: 2rem;
} }
.card { .section-title {
background-color: hsl(var(--card)); font-size: 1.5rem;
font-weight: 600;
color: hsl(var(--foreground));
margin-bottom: 1.5rem;
display: flex;
align-items: center;
gap: 0.75rem;
}
.section-title i {
color: #6366f1;
}
.stat-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.stat-card {
background: hsl(var(--card));
border: 1px solid hsl(var(--border)); border: 1px solid hsl(var(--border));
border-radius: var(--radius); border-radius: 12px;
padding: 2rem; padding: 1.5rem;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.stat-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, #6366f1, #8b5cf6);
}
.stat-card:hover {
transform: translateY(-4px);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
}
.stat-card-icon {
font-size: 2rem;
margin-bottom: 1rem;
color: #6366f1;
}
.stat-card-title {
font-size: 0.875rem;
font-weight: 500;
color: hsl(var(--muted-foreground));
margin-bottom: 0.5rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.stat-card-value {
font-size: 1.75rem;
font-weight: 700;
color: hsl(var(--foreground));
}
.quick-actions {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-bottom: 2rem;
}
.quick-action {
background: hsl(var(--card));
border: 1px solid hsl(var(--border));
border-radius: 12px;
padding: 1.5rem;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
text-decoration: none;
color: hsl(var(--foreground));
}
.quick-action:hover {
transform: translateY(-4px);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
text-decoration: none;
color: hsl(var(--foreground));
}
.quick-action-icon {
font-size: 2rem;
margin-bottom: 1rem;
color: #6366f1;
}
.quick-action-title {
font-weight: 600;
margin-bottom: 0.5rem;
}
.quick-action-desc {
font-size: 0.85rem;
color: hsl(var(--muted-foreground));
} }
.btn { .btn {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
border-radius: var(--radius); gap: 0.5rem;
padding: 0.875rem 1.5rem;
border-radius: 25px;
font-weight: 500; font-weight: 500;
font-size: 0.95rem;
transition: all 0.3s ease; transition: all 0.3s ease;
cursor: pointer; cursor: pointer;
border: none;
text-decoration: none;
position: relative;
overflow: hidden;
}
.btn::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
transition: left 0.5s;
}
.btn:hover::before {
left: 100%;
}
.btn:active {
transform: scale(0.98);
} }
.btn-primary { .btn-primary {
background-color: hsl(var(--primary)); background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
color: hsl(var(--primary-foreground)); color: white;
box-shadow: 0 4px 14px 0 rgba(99, 102, 241, 0.3);
} }
.btn-primary:hover { .btn-primary:hover {
opacity: 0.9; transform: translateY(-2px);
box-shadow: 0 8px 25px 0 rgba(99, 102, 241, 0.4);
} }
.btn-secondary { .btn-secondary {
background-color: hsl(var(--secondary)); background: hsl(var(--secondary));
color: hsl(var(--secondary-foreground)); color: hsl(var(--secondary-foreground));
border: 1px solid hsl(var(--border));
} }
.btn-secondary:hover { .btn-secondary:hover {
opacity: 0.9; background: hsl(var(--accent));
color: hsl(var(--accent-foreground));
transform: translateY(-1px);
} }
#themeSwitcher { .btn-full {
width: 100%;
}
.theme-switcher {
position: fixed; position: fixed;
top: 1rem; top: 2rem;
right: 1rem; right: 2rem;
z-index: 10;
background: hsl(var(--card));
border: 1px solid hsl(var(--border));
border-radius: 50%;
padding: 0.75rem;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.1);
} }
.animate { .theme-switcher:hover {
animation: fadeIn 0.5s ease-out; transform: scale(1.1);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
} }
@keyframes fadeIn { .theme-switcher svg {
from { opacity: 0; } width: 1.25rem;
to { opacity: 1; } height: 1.25rem;
color: hsl(var(--foreground));
} }
.modal { .modal {
display: none; display: none;
position: fixed; position: fixed;
z-index: 1; z-index: 1000;
left: 0; left: 0;
top: 0; top: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
overflow: auto; overflow: auto;
background-color: rgba(0,0,0,0.4); background-color: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(4px);
} }
.modal-content { .modal-content {
background-color: hsl(var(--background)); background: hsl(var(--card));
margin: 15% auto; margin: 5% auto;
padding: 20px; padding: 0;
border: 1px solid hsl(var(--border)); border: 1px solid hsl(var(--border));
width: 80%; width: 90%;
border-radius: var(--radius); max-width: 900px;
border-radius: 16px;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
animation: slideIn 0.3s ease-out;
}
.modal-header {
padding: 1.5rem 2rem;
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
color: white;
border-radius: 16px 16px 0 0;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h2 {
margin: 0;
font-size: 1.5rem;
font-weight: 600;
} }
.close { .close {
color: hsl(var(--muted-foreground)); color: white;
float: right; font-size: 2rem;
font-size: 28px; font-weight: 300;
font-weight: bold; cursor: pointer;
transition: all 0.2s ease;
line-height: 1;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
} }
.close:hover, .close:hover {
.close:focus { background: rgba(255, 255, 255, 0.2);
transform: rotate(90deg);
}
.modal-body {
padding: 2rem;
max-height: 70vh;
overflow-y: auto;
}
.modal-body pre {
background: hsl(var(--muted));
border: 1px solid hsl(var(--border));
border-radius: 8px;
padding: 1.5rem;
overflow-x: auto;
font-size: 0.875rem;
line-height: 1.6;
color: hsl(var(--foreground)); color: hsl(var(--foreground));
text-decoration: none; font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
cursor: pointer; }
.chart-container {
background: hsl(var(--card));
border: 1px solid hsl(var(--border));
border-radius: 12px;
padding: 1.5rem;
margin-bottom: 2rem;
}
.chart-container canvas {
max-height: 300px;
}
@media (max-width: 768px) {
.container {
padding: 1rem;
}
.admin-header {
padding: 1.5rem;
}
.admin-header h1 {
font-size: 1.5rem;
}
.admin-content {
padding: 1.5rem;
}
.theme-switcher {
top: 1rem;
right: 1rem;
}
.stat-cards {
grid-template-columns: 1fr;
}
.modal-content {
width: 95%;
margin: 10% auto;
}
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
} }
</style> </style>
</head> </head>
<body class="animate"> <body class="dark">
<div id="app" class="min-h-screen flex items-center justify-center"> <div class="backdrop"></div>
<div class="container mt-8">
<h1 class="text-3xl font-semibold mb-6 text-center animate">Statistiques du serveur</h1>
<div class="card mb-8"> <div class="container">
<h2 class="text-2xl font-semibold mb-4">Informations générales</h2> <div class="admin-card">
<table class="w-full"> <div class="admin-header">
<thead> <h1><i class="fas fa-chart-line"></i> Statistiques & Logs</h1>
<tr>
<th class="text-left">Paramètre</th>
<th class="text-left">Valeur</th>
</tr>
</thead>
<tbody>
<tr>
<td>Temps de fonctionnement</td>
<td><%= Math.floor(uptime / 86400) %> jours, <%= Math.floor(uptime % 86400 / 3600) %> heures, <%= Math.floor(uptime % 3600 / 60) %> minutes, <%= uptime % 60 %> secondes</td>
</tr>
<tr>
<td>Utilisation de la mémoire</td>
<td><%= memoryUsage.toFixed(2) %> Mo</td>
</tr>
<tr>
<td>Utilisation du processeur</td>
<td><%= (cpuUsage * 100).toFixed(2) %> %</td>
</tr>
</tbody>
</table>
</div> </div>
<div class="card"> <div class="admin-content">
<h2 class="text-2xl font-semibold mb-4">Journaux</h2> <!-- Statistiques Système -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4"> <div class="section-title">
<% logs && logs.forEach((log, index) => { %> <i class="fas fa-server"></i>
<% if (log) { %> Statistiques Système
<button class="btn btn-primary logName" data-index="<%= index %>"><%= log.name %></button>
<div id="myModal<%= index %>" class="modal">
<div class="modal-content">
<span class="close" data-index="<%= index %>">&times;</span>
<pre class="whitespace-pre-wrap"><%= log.content %></pre>
</div>
</div>
<% } %>
<% }); %>
</div> </div>
</div>
<div class="mt-8 flex justify-center"> <div class="stat-cards">
<a href="/dpanel/dashboard/admin/" class="btn btn-secondary w-full py-2 mt-4 text-center"> <div class="stat-card">
<i class="fas fa-arrow-left mr-2"></i> <div class="stat-card-icon">
Retourner au dashboard admin <i class="fas fa-clock"></i>
</a> </div>
<div class="stat-card-title">Uptime</div>
<div class="stat-card-value" id="uptimeDisplay">
<%= Math.floor(uptime / 86400) %>j <%= Math.floor(uptime % 86400 / 3600) %>h <%= Math.floor(uptime % 3600 / 60) %>m
</div>
</div>
<div class="stat-card">
<div class="stat-card-icon">
<i class="fas fa-memory"></i>
</div>
<div class="stat-card-title">Mémoire</div>
<div class="stat-card-value">
<%= memoryUsage.toFixed(2) %> Mo
</div>
</div>
<div class="stat-card">
<div class="stat-card-icon">
<i class="fas fa-microchip"></i>
</div>
<div class="stat-card-title">CPU</div>
<div class="stat-card-value">
<%= (cpuUsage * 100).toFixed(2) %>%
</div>
</div>
</div>
<!-- Graphiques -->
<div class="chart-container">
<canvas id="systemChart"></canvas>
</div>
<!-- Journaux -->
<div class="section-title">
<i class="fas fa-file-alt"></i>
Journaux du Système
</div>
<div class="quick-actions">
<% if (logs && logs.length > 0) { %>
<% logs.forEach((log, index) => { %>
<% if (log) { %>
<div class="quick-action" data-log-index="<%= index %>">
<div class="quick-action-icon">
<i class="fas fa-file-code"></i>
</div>
<div class="quick-action-title"><%= log.name %></div>
<div class="quick-action-desc">Cliquer pour voir</div>
</div>
<% } %>
<% }); %>
<% } else { %>
<div style="grid-column: 1 / -1; text-align: center; padding: 2rem; color: hsl(var(--muted-foreground));">
<i class="fas fa-info-circle" style="font-size: 3rem; margin-bottom: 1rem; opacity: 0.5;"></i>
<p>Aucun log disponible pour le moment.</p>
</div>
<% } %>
</div>
<!-- Bouton retour -->
<div style="margin-top: 2rem;">
<a href="/dpanel/dashboard/admin/" class="btn btn-secondary btn-full">
<i class="fas fa-arrow-left"></i>
Retourner au dashboard admin
</a>
</div>
</div> </div>
</div> </div>
</div> </div>
<button id="themeSwitcher" class="btn btn-secondary p-2"> <!-- Modals pour les logs -->
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6"> <% if (logs && logs.length > 0) { %>
<% logs.forEach((log, index) => { %>
<% if (log) { %>
<div id="logModal<%= index %>" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2><i class="fas fa-file-code"></i> <%= log.name %></h2>
<span class="close" data-modal-index="<%= index %>">&times;</span>
</div>
<div class="modal-body">
<pre><%= log.content %></pre>
</div>
</div>
</div>
<% } %>
<% }); %>
<% } %>
<!-- Theme Switcher -->
<button class="theme-switcher" id="themeSwitcher">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" />
</svg> </svg>
</button> </button>
<script> <script>
// =================== VARIABLES GLOBALES ===================
const body = document.body; const body = document.body;
const themeSwitcher = document.getElementById('themeSwitcher'); const themeSwitcher = document.getElementById('themeSwitcher');
// =================== GESTION DU THÈME ===================
function setTheme(theme) { function setTheme(theme) {
if (theme === 'dark') { if (theme === 'dark') {
body.classList.add('dark'); body.classList.add('dark');
@@ -258,30 +630,127 @@
} }
}); });
var logNames = document.getElementsByClassName("logName"); // =================== GESTION DES MODALS ===================
var closeButtons = document.getElementsByClassName("close"); const logButtons = document.querySelectorAll('.quick-action[data-log-index]');
const closeButtons = document.querySelectorAll('.close[data-modal-index]');
for (var i = 0; i < logNames.length; i++) { logButtons.forEach(button => {
logNames[i].addEventListener("click", function(event) { button.addEventListener('click', function() {
var index = event.target.getAttribute("data-index"); const index = this.getAttribute('data-log-index');
var modal = document.getElementById("myModal" + index); const modal = document.getElementById('logModal' + index);
modal.style.display = "block"; if (modal) {
modal.style.display = 'block';
}
}); });
} });
for (var i = 0; i < closeButtons.length; i++) { closeButtons.forEach(button => {
closeButtons[i].addEventListener("click", function(event) { button.addEventListener('click', function() {
var index = event.target.getAttribute("data-index"); const index = this.getAttribute('data-modal-index');
var modal = document.getElementById("myModal" + index); const modal = document.getElementById('logModal' + index);
modal.style.display = "none"; if (modal) {
modal.style.display = 'none';
}
}); });
} });
window.onclick = function(event) { window.onclick = function(event) {
if (event.target.className === "modal") { if (event.target.classList.contains('modal')) {
event.target.style.display = "none"; event.target.style.display = 'none';
} }
} };
// =================== GRAPHIQUE CHART.JS ===================
document.addEventListener('DOMContentLoaded', function() {
const ctx = document.getElementById('systemChart');
if (ctx) {
const isDark = body.classList.contains('dark');
const textColor = isDark ? 'rgba(210, 214, 220, 0.8)' : 'rgba(34, 41, 47, 0.8)';
const gridColor = isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
new Chart(ctx, {
type: 'bar',
data: {
labels: ['Uptime (jours)', 'Mémoire (Mo)', 'CPU (%)'],
datasets: [{
label: 'Statistiques Système',
data: [
<%= Math.floor(uptime / 86400) %>,
<%= memoryUsage.toFixed(2) %>,
<%= (cpuUsage * 100).toFixed(2) %>
],
backgroundColor: [
'rgba(99, 102, 241, 0.6)',
'rgba(139, 92, 246, 0.6)',
'rgba(168, 85, 247, 0.6)'
],
borderColor: [
'rgba(99, 102, 241, 1)',
'rgba(139, 92, 246, 1)',
'rgba(168, 85, 247, 1)'
],
borderWidth: 2,
borderRadius: 8
}]
},
options: {
responsive: true,
maintainAspectRatio: true,
plugins: {
legend: {
display: true,
labels: {
color: textColor,
font: {
family: 'Inter',
size: 12
}
}
},
title: {
display: true,
text: 'Aperçu des Ressources Système',
color: textColor,
font: {
family: 'Inter',
size: 16,
weight: '600'
}
}
},
scales: {
y: {
beginAtZero: true,
ticks: {
color: textColor
},
grid: {
color: gridColor
}
},
x: {
ticks: {
color: textColor
},
grid: {
color: gridColor
}
}
}
}
});
}
});
// =================== ANIMATIONS ===================
document.addEventListener('DOMContentLoaded', function() {
setTimeout(() => {
document.querySelector('.admin-card').style.opacity = '1';
document.querySelector('.admin-card').style.transform = 'translateY(0) scale(1)';
}, 100);
console.log('📊 Interface Statistiques & Logs chargée !');
});
</script> </script>
</body> </body>
</html> </html>

File diff suppressed because it is too large Load Diff

View File

@@ -592,12 +592,6 @@
<div class="backdrop"></div> <div class="backdrop"></div>
<div class="container"> <div class="container">
<!-- Notice Interface en Test -->
<div class="beta-notice">
<i class="fas fa-flask"></i>
<strong>Interface en cours de test</strong> - Cette nouvelle interface moderne est actuellement en phase de test.
Selon les retours des utilisateurs, elle sera déployée sur toute l'application (gestion admin, téléversement, etc.).
</div>
<div class="profile-card"> <div class="profile-card">
<!-- Header du profil --> <!-- Header du profil -->