Update v1.2.0-beta - Dynamic context menu & permissions
All checks were successful
continuous-integration/drone/push Build is passing
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:
8
.claude/settings.local.json
Normal file
8
.claude/settings.local.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(npm audit:*)"
|
||||||
|
],
|
||||||
|
"deny": []
|
||||||
|
}
|
||||||
|
}
|
||||||
234
CLAUDE.md
Normal file
234
CLAUDE.md
Normal 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
|
||||||
170
Middlewares/csrfProtectionMiddleware.js
Normal file
170
Middlewares/csrfProtectionMiddleware.js
Normal 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
|
||||||
|
};
|
||||||
202
Middlewares/inputValidationMiddleware.js
Normal file
202
Middlewares/inputValidationMiddleware.js
Normal 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, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''')
|
||||||
|
.replace(/\//g, '/');
|
||||||
|
|
||||||
|
return str.trim();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valide un nom de fichier/dossier
|
||||||
|
*/
|
||||||
|
const validateFileName = (filename) => {
|
||||||
|
if (!filename || typeof filename !== 'string') {
|
||||||
|
return { valid: false, error: 'Nom de fichier invalide' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limite de longueur
|
||||||
|
if (filename.length > 255) {
|
||||||
|
return { valid: false, error: 'Nom de fichier trop long (max 255 caractères)' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Caractères interdits dans les noms de fichiers Windows/Linux
|
||||||
|
const forbiddenChars = /[<>:"\/\\|?*\x00-\x1f]/g;
|
||||||
|
if (forbiddenChars.test(filename)) {
|
||||||
|
return { valid: false, error: 'Caractères interdits dans le nom de fichier' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Noms réservés Windows
|
||||||
|
const reservedNames = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i;
|
||||||
|
if (reservedNames.test(filename.split('.')[0])) {
|
||||||
|
return { valid: false, error: 'Nom de fichier réservé' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier les tentatives de traversée de répertoire
|
||||||
|
if (filename.includes('..') || filename.includes('./') || filename.includes('.\\')) {
|
||||||
|
return { valid: false, error: 'Tentative de traversée de répertoire détectée' };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { valid: true };
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valide un chemin de dossier
|
||||||
|
*/
|
||||||
|
const validatePath = (path) => {
|
||||||
|
if (!path || typeof path !== 'string') {
|
||||||
|
return { valid: false, error: 'Chemin invalide' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier les tentatives de traversée de répertoire
|
||||||
|
if (path.includes('..') || path.includes('~')) {
|
||||||
|
return { valid: false, error: 'Tentative de traversée de répertoire détectée' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier les chemins absolus non autorisés
|
||||||
|
if (path.startsWith('/') || /^[A-Za-z]:/.test(path)) {
|
||||||
|
return { valid: false, error: 'Chemin absolu non autorisé' };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { valid: true };
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valide un ID utilisateur
|
||||||
|
*/
|
||||||
|
const validateUserId = (userId) => {
|
||||||
|
if (!userId) {
|
||||||
|
return { valid: false, error: 'ID utilisateur manquant' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accepte les formats Discord (snowflake) ou alphanumériques
|
||||||
|
if (!/^[a-zA-Z0-9_-]{1,100}$/.test(userId)) {
|
||||||
|
return { valid: false, error: 'Format d\'ID utilisateur invalide' };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { valid: true };
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valide une adresse email
|
||||||
|
*/
|
||||||
|
const validateEmail = (email) => {
|
||||||
|
if (!email || typeof email !== 'string') {
|
||||||
|
return { valid: false, error: 'Email invalide' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
if (!emailRegex.test(email)) {
|
||||||
|
return { valid: false, error: 'Format d\'email invalide' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (email.length > 254) {
|
||||||
|
return { valid: false, error: 'Email trop long' };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { valid: true };
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nettoie récursivement un objet
|
||||||
|
*/
|
||||||
|
const sanitizeObject = (obj) => {
|
||||||
|
if (obj === null || obj === undefined) return obj;
|
||||||
|
|
||||||
|
if (typeof obj === 'string') {
|
||||||
|
return sanitizeString(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(obj)) {
|
||||||
|
return obj.map(item => sanitizeObject(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof obj === 'object') {
|
||||||
|
const sanitized = {};
|
||||||
|
for (const [key, value] of Object.entries(obj)) {
|
||||||
|
sanitized[key] = sanitizeObject(value);
|
||||||
|
}
|
||||||
|
return sanitized;
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Middleware principal de validation
|
||||||
|
*/
|
||||||
|
const inputValidationMiddleware = (req, res, next) => {
|
||||||
|
try {
|
||||||
|
// Log des requêtes suspectes
|
||||||
|
const suspiciousPatterns = [
|
||||||
|
/<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
|
||||||
|
};
|
||||||
194
Middlewares/rateLimitMiddleware.js
Normal file
194
Middlewares/rateLimitMiddleware.js
Normal 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
|
||||||
|
};
|
||||||
76
Middlewares/securityHeadersMiddleware.js
Normal file
76
Middlewares/securityHeadersMiddleware.js
Normal 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;
|
||||||
@@ -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
725
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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
457
public/css/admin.styles.css
Normal 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);
|
||||||
|
}
|
||||||
@@ -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 !important;
|
||||||
|
background: hsl(var(--accent)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; }
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes modalSlideIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-20px) scale(0.95);
|
||||||
|
}
|
||||||
|
to {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar {
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 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 {
|
||||||
|
|||||||
12
public/css/dropdown-fixes.css
Normal file
12
public/css/dropdown-fixes.css
Normal 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;
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
document.querySelectorAll('.context-menu .menu-item').forEach(item => {
|
|
||||||
item.addEventListener('click', function(e) {
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const action = this.dataset.action;
|
const target = e.target.closest('button, a');
|
||||||
if (selectedItem) {
|
if (!target || !selectedItem) return;
|
||||||
|
|
||||||
|
let action = '';
|
||||||
|
if (target.classList.contains('context-item-open')) action = 'open';
|
||||||
|
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);
|
handleMenuAction(action, selectedItem);
|
||||||
}
|
|
||||||
hideContextMenu();
|
hideContextMenu();
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function adjustMenuOptions(item) {
|
|
||||||
const menuItems = contextMenu.querySelectorAll('.menu-item');
|
|
||||||
|
|
||||||
menuItems.forEach(menuItem => {
|
|
||||||
const action = menuItem.dataset.action;
|
|
||||||
|
|
||||||
switch(action) {
|
|
||||||
case 'open':
|
|
||||||
menuItem.style.display = item.type.includes('folder') ? 'flex' : 'none';
|
|
||||||
break;
|
|
||||||
case 'collaborate':
|
|
||||||
menuItem.style.display = item.type === 'folder' ? 'flex' : 'none';
|
|
||||||
const collabBtn = item.element.querySelector('.toggle-collaboration-btn');
|
|
||||||
const isCollaborative = collabBtn?.dataset.isCollaborative === 'true';
|
|
||||||
menuItem.querySelector('span').textContent =
|
|
||||||
isCollaborative ? 'Gérer la collaboration' : 'Activer la collaboration';
|
|
||||||
break;
|
|
||||||
case 'copy-link':
|
|
||||||
menuItem.style.display = item.type === 'file' ? 'flex' : 'none';
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
}); 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
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Vérifier si l'utilisateur est propriétaire (pour dossiers partagés)
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
function showContextMenu(x, y) {
|
// Récupérer tous les éléments du menu
|
||||||
|
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');
|
||||||
|
|
||||||
|
// MASQUER TOUT PAR DÉFAUT
|
||||||
|
if (openBtn) openBtn.style.display = 'none';
|
||||||
|
if (renameBtn) renameBtn.style.display = 'none';
|
||||||
|
if (collaborateBtn) collaborateBtn.style.display = 'none';
|
||||||
|
if (shareBtn) shareBtn.style.display = 'none';
|
||||||
|
if (moveBtn) moveBtn.style.display = 'none';
|
||||||
|
if (leaveBtn) leaveBtn.style.display = 'none';
|
||||||
|
if (separator) separator.style.display = 'none';
|
||||||
|
if (deleteBtn) deleteBtn.style.display = 'none';
|
||||||
|
|
||||||
|
// AFFICHER SELON LE TYPE
|
||||||
|
if (isFile) {
|
||||||
|
// FICHIERS : Renommer, Copier le lien, Déplacer, Supprimer
|
||||||
|
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';
|
||||||
|
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,13 +61,16 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
darkModeMediaQuery.addListener(applyStyleMode);
|
darkModeMediaQuery.addListener(applyStyleMode);
|
||||||
applyStyleMode();
|
applyStyleMode();
|
||||||
|
|
||||||
|
if (styleSwitcherButton) {
|
||||||
styleSwitcherButton.addEventListener('click', toggleDarkMode);
|
styleSwitcherButton.addEventListener('click', toggleDarkMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filterForm) {
|
||||||
filterForm.addEventListener('submit', function (event) {
|
filterForm.addEventListener('submit', function (event) {
|
||||||
event.preventDefault();
|
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]');
|
||||||
|
|
||||||
@@ -96,6 +88,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
async function confirmDeleteFile(folderName, filename) {
|
async function confirmDeleteFile(folderName, filename) {
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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.' });
|
||||||
}
|
}
|
||||||
|
|||||||
36
routes/Dpanel/API/SharedFolders.js
Normal file
36
routes/Dpanel/API/SharedFolders.js
Normal 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;
|
||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -116,7 +116,11 @@ router.get('/:userId/:filename', async (req, res) => {
|
|||||||
await pipeline(readStream, res);
|
await pipeline(readStream, res);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (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);
|
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.');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
24
server.js
24
server.js
@@ -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());
|
||||||
|
|||||||
@@ -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 w-full text-left px-4 py-2 hover:bg-gray-100 flex items-center">
|
<button class="context-item-rename menu-item">
|
||||||
<i class="fas fa-edit mr-2"></i> Renommer
|
<i class="fas fa-edit"></i> <span>Renommer</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="context-item-collaborate w-full text-left px-4 py-2 hover:bg-gray-100 flex items-center">
|
<button class="context-item-collaborate menu-item">
|
||||||
<i class="fas fa-users mr-2"></i> Collaborer
|
<i class="fas fa-users"></i> <span>Collaborer</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="context-item-share w-full text-left px-4 py-2 hover:bg-gray-100 flex items-center">
|
<button class="context-item-share menu-item">
|
||||||
<i class="fas fa-share-alt mr-2"></i> Copier le lien
|
<i class="fas fa-share-alt"></i> <span>Copier le lien</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="context-item-move w-full text-left px-4 py-2 hover:bg-gray-100 flex items-center">
|
<button class="context-item-move menu-item">
|
||||||
<i class="fas fa-file-export mr-2"></i> Déplacer
|
<i class="fas fa-file-export"></i> <span>Déplacer</span>
|
||||||
</button>
|
</button>
|
||||||
<div class="border-t border-gray-200 my-2"></div>
|
<button class="context-item-leave menu-item" style="color: #f59e0b;">
|
||||||
<button class="context-item-delete w-full text-left px-4 py-2 hover:bg-gray-100 flex items-center text-red-600">
|
<i class="fas fa-sign-out-alt"></i> <span>Quitter ce dossier</span>
|
||||||
<i class="fas fa-trash-alt mr-2"></i> Supprimer
|
</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>
|
</button>
|
||||||
</div>
|
|
||||||
</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">×</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">×</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>
|
||||||
850
views/folder.ejs
850
views/folder.ejs
@@ -1,75 +1,132 @@
|
|||||||
<!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" />
|
||||||
</head>
|
<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>
|
<style>
|
||||||
|
|
||||||
body {
|
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-image: url('<%= user.wallpaper %>');
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-attachment: fixed;
|
background-attachment: fixed;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
height: 100vh;
|
min-height: 100vh;
|
||||||
overflow: hidden;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="animate">
|
||||||
|
|
||||||
|
<nav class="navbar navbar-expand-md navbar-light bg-light header">
|
||||||
<body class="light-mode">
|
<div class="container-fluid">
|
||||||
<nav class="navbar navbar-expand-lg navbar-light bg-light header">
|
<a class="navbar-brand" href="/dpanel/dashboard">
|
||||||
<a class="navbar-brand">
|
|
||||||
Dashboard CDN
|
Dashboard CDN
|
||||||
<span class="badge badge-info ml-1">Beta</span>
|
<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>
|
||||||
</a>
|
</a>
|
||||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav"
|
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav"
|
||||||
aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
<span class="navbar-toggler-icon"></span>
|
<span class="navbar-toggler-icon"></span>
|
||||||
</button>
|
</button>
|
||||||
<div class="collapse navbar-collapse ml-auto" 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">
|
||||||
<button class="btn btn-warning btn-round mr-2 animated-button" onclick="window.location.href='/dpanel/dashboard';">
|
<button type="button" class="btn btn-primary" id="uploadToFolderBtn">
|
||||||
<i class="fas fa-home"></i>Page principal</button>
|
<i class="fas fa-cloud-upload-alt"></i> Téléverser ici
|
||||||
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<form action="/dpanel/upload" class="form-inline">
|
<button type="button" class="btn btn-success" id="newFolderBtn">
|
||||||
<button class="btn btn-primary btn-round mr-2 animated-button">
|
<i class="fas fa-folder-open"></i> Nouveau
|
||||||
<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 dropdown">
|
||||||
<button id="styleSwitcher" class="btn btn-link btn-round animated-button">
|
<button class="btn dropdown-toggle nav-btn" id="accountDropdownBtn" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||||
<span id="themeIcon" class="fas theme-icon"></span>
|
<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>
|
</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>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
<nav aria-label="breadcrumb">
|
</nav>
|
||||||
|
|
||||||
|
|
||||||
|
<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">
|
<ol class="breadcrumb custom-breadcrumb">
|
||||||
<li class="breadcrumb-item"><a href="/dpanel/dashboard">Accueil</a></li>
|
<li class="breadcrumb-item"><a href="/dpanel/dashboard">Accueil</a></li>
|
||||||
<% let pathSegments = currentFolder.split('/'); %>
|
<% if (currentFolder) { %>
|
||||||
|
<% let pathSegments = currentFolder.split('/').filter(s => s); %>
|
||||||
<% pathSegments.forEach((segment, index) => { %>
|
<% pathSegments.forEach((segment, index) => { %>
|
||||||
<% let pathSoFar = pathSegments.slice(0, index + 1).join('/'); %>
|
<% let pathSoFar = pathSegments.slice(0, index + 1).join('/'); %>
|
||||||
<li class="breadcrumb-item <%= (index === pathSegments.length - 1) ? 'active' : '' %>">
|
<li class="breadcrumb-item <%= (index === pathSegments.length - 1) ? 'active' : '' %>">
|
||||||
@@ -80,96 +137,71 @@
|
|||||||
<% } %>
|
<% } %>
|
||||||
</li>
|
</li>
|
||||||
<% }); %>
|
<% }); %>
|
||||||
|
<% } %>
|
||||||
</ol>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
</body>
|
|
||||||
|
|
||||||
<% function formatSize(sizeInBytes) {
|
<div class="form-container">
|
||||||
if (sizeInBytes < 1024) {
|
<div class="flex justify-between items-center mb-4">
|
||||||
return `${sizeInBytes} octets`;
|
<input type="text" id="searchInput" class="form-control w-1/2" placeholder="Rechercher par nom de fichier">
|
||||||
} else if (sizeInBytes < 1024 * 1024) {
|
<button id="searchButton" class="btn btn-primary">Rechercher</button>
|
||||||
return `${(sizeInBytes / 1024).toFixed(2)} Ko`;
|
</div>
|
||||||
} else if (sizeInBytes < 1024 * 1024 * 1024) {
|
|
||||||
return `${(sizeInBytes / (1024 * 1024)).toFixed(2)} Mo`;
|
|
||||||
} else {
|
|
||||||
return `${(sizeInBytes / (1024 * 1024 * 1024)).toFixed(2)} Go`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
%>
|
|
||||||
|
|
||||||
<div class="container mt-4 table-container">
|
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table w-100">
|
<table class="table" id="fileTable">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Nom du fichier</th>
|
<th>Nom</th>
|
||||||
<th>Taille</th>
|
<th class="text-center">Type</th>
|
||||||
<th class="text-right">Action</th>
|
<th class="text-center">Taille</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<% files.forEach(file => { %>
|
<% files.forEach(file => { %>
|
||||||
<tr data-extension="<%= file.extension %>" data-type="<%= file.type %>">
|
<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;">
|
||||||
<% if (fileInfoNames.includes(file.name)) { %>
|
|
||||||
<td><a href="#" onclick="showFileInfo('<%= file.name %>')"><%= file.name %></a></td>
|
|
||||||
<% } else { %>
|
|
||||||
<td><%= file.name %></td>
|
|
||||||
<% } %>
|
|
||||||
<td>
|
<td>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
<% if (file.type === 'folder') { %>
|
<% if (file.type === 'folder') { %>
|
||||||
<% const folderSize = calculateFolderSize(file.contents); %>
|
<i class="fas fa-folder text-warning mr-2"></i>
|
||||||
<%= (folderSize !== undefined && !isNaN(folderSize)) ? formatSize(folderSize) : 'Taille inconnue' %>
|
<%= file.name %>
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
<%
|
<i class="fas fa-file text-secondary mr-2"></i>
|
||||||
const fileSizeInBytes = file.size;
|
<%= file.name %>
|
||||||
let fileSize;
|
<% } %>
|
||||||
if (fileSizeInBytes !== undefined && !isNaN(fileSizeInBytes) && fileSizeInBytes >= 0) {
|
</div>
|
||||||
fileSize = formatSize(fileSizeInBytes);
|
</td>
|
||||||
} else {
|
<td class="text-center">
|
||||||
console.error('Invalid file size:', fileSizeInBytes);
|
<% if (file.type === 'folder') { %>
|
||||||
fileSize = 'Taille inconnue';
|
<span class="badge badge-warning">Dossier</span>
|
||||||
}
|
<% } else { %>
|
||||||
%>
|
<span class="badge badge-secondary">Fichier</span>
|
||||||
<%= fileSize %>
|
|
||||||
<% } %>
|
<% } %>
|
||||||
</td>
|
</td>
|
||||||
<td class="d-flex justify-content-end align-items-center">
|
<td class="text-center">
|
||||||
<% if (file.type === 'folder') { %>
|
<% 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
|
function calculateFolderSize(contents) {
|
||||||
</a>
|
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 { %>
|
<% } else { %>
|
||||||
<button class="btn btn-primary btn-round animated-button" onclick="renameFile('<%= folderName %>', '<%= file.name %>')">
|
<span class="file-size" data-size="<%= file.size %>">
|
||||||
<i class="fas fa-edit fa-xs btn-icon"></i> Renommer
|
<%= file.size %> octets
|
||||||
</button>
|
</span>
|
||||||
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -178,24 +210,598 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="footer mt-auto py-3 bg-light">
|
||||||
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"></script>
|
<div class="container">
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"></script>
|
<span class="text-muted">Version: <span id="version-number">...</span> | © <span id="current-year"></span> Myaxrin Labs</span>
|
||||||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"></script>
|
<a href="#" class="float-right" onclick="displayMetadata()">Metadata</a>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
document.getElementById('current-year').textContent = new Date().getFullYear();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="container">
|
<div class="modal fade" id="metadataModal" tabindex="-1" role="dialog" aria-labelledby="metadataModalLabel" aria-hidden="true">
|
||||||
<footer class="py-3 my-4">
|
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||||
<ul class="nav justify-content-center border-bottom pb-3 mb-3">
|
<div class="modal-content">
|
||||||
<li class="nav-item"><a class="nav-link px-2 text-muted">Version: <span id="version-number">...</span></a></li>
|
<div class="modal-header">
|
||||||
</ul>
|
<h5 class="modal-title" id="metadataModalLabel">Metadata</h5>
|
||||||
<p class="text-center text-muted">© 2024 Myaxrin Labs</p>
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||||
</footer>
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p><i class="fas fa-code-branch"></i> Version de Build: <span id="buildVersion"></span></p>
|
||||||
|
<p><i class="fab fa-node"></i> Version de Node.js: <span id="nodeVersion"></span></p>
|
||||||
|
<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>
|
||||||
|
<p><i class="fas fa-windows"></i> Type d'OS: <span id="osType"></span></p>
|
||||||
|
<p><i class="fas fa-laptop-code"></i> Version d'OS: <span id="osRelease"></span></p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-dismiss="modal">Fermer</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
|
||||||
|
|
||||||
|
<div class="modal fade" id="moveFileModal" tabindex="-1" role="dialog" aria-labelledby="moveFileModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Déplacer le fichier</h5>
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-label="Fermer">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="moveFileForm">
|
||||||
|
<input type="hidden" id="moveFileName" name="fileName">
|
||||||
|
<input type="hidden" id="moveUserName" name="userName" value="<%= userName %>">
|
||||||
|
<input type="hidden" id="moveOldFolderName" name="oldFolderName" value="<%= folderName %>">
|
||||||
|
<select class="form-control" id="moveFolderSelect" name="newFolderName">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<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">×</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>
|
||||||
|
|||||||
@@ -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: 2rem;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
top: 1rem;
|
||||||
right: 1rem;
|
right: 1rem;
|
||||||
z-index: 1000; /* Ensure it stays above other elements */
|
|
||||||
background-color: hsl(var(--secondary));
|
|
||||||
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 {
|
.stats-grid {
|
||||||
background-color: hsl(var(--primary));
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
#themeSwitcher svg {
|
.quick-actions {
|
||||||
width: 24px;
|
grid-template-columns: 1fr;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Toast notifications */
|
||||||
.swal2-toast {
|
.swal2-toast {
|
||||||
background-color: hsl(var(--card));
|
background-color: hsl(var(--card)) !important;
|
||||||
color: hsl(var(--foreground));
|
color: hsl(var(--foreground)) !important;
|
||||||
border: 1px solid hsl(var(--border));
|
border: 1px solid hsl(var(--border)) !important;
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius) !important;
|
||||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
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">
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="profile-card">
|
||||||
|
<!-- 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>
|
||||||
|
|
||||||
|
<!-- Onglets -->
|
||||||
|
<div class="tabs-container">
|
||||||
|
<div class="tabs">
|
||||||
|
<button class="tab active" data-tab="overview">
|
||||||
|
<i class="fas fa-chart-pie"></i>
|
||||||
|
Aperçu
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<!-- 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>
|
||||||
|
|
||||||
<div class="min-h-screen flex items-center justify-center p-4">
|
|
||||||
<div class="container mt-8">
|
|
||||||
<h1 class="text-3xl font-semibold mb-6 text-center">Paramètres Admin</h1>
|
|
||||||
|
|
||||||
<div class="form-container">
|
|
||||||
<div class="flex flex-col gap-4">
|
|
||||||
<a href="/dpanel/dashboard/admin/users?page=1&limit=10" class="btn btn-primary flex items-center justify-center space-x-2">
|
|
||||||
<i class="fas fa-users icon-spacing"></i>
|
|
||||||
<span>Gérer les utilisateurs</span>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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>
|
||||||
|
|||||||
@@ -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 %>">×</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 %>">×</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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
Statistiques Système
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat-cards">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-card-icon">
|
||||||
|
<i class="fas fa-clock"></i>
|
||||||
|
</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) { %>
|
<% if (log) { %>
|
||||||
<button class="btn btn-primary logName" data-index="<%= index %>"><%= log.name %></button>
|
<div class="quick-action" data-log-index="<%= index %>">
|
||||||
<div id="myModal<%= index %>" class="modal">
|
<div class="quick-action-icon">
|
||||||
<div class="modal-content">
|
<i class="fas fa-file-code"></i>
|
||||||
<span class="close" data-index="<%= index %>">×</span>
|
|
||||||
<pre class="whitespace-pre-wrap"><%= log.content %></pre>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="quick-action-title"><%= log.name %></div>
|
||||||
|
<div class="quick-action-desc">Cliquer pour voir</div>
|
||||||
</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>
|
||||||
|
<% } %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-8 flex justify-center">
|
<!-- Bouton retour -->
|
||||||
<a href="/dpanel/dashboard/admin/" class="btn btn-secondary w-full py-2 mt-4 text-center">
|
<div style="margin-top: 2rem;">
|
||||||
<i class="fas fa-arrow-left mr-2"></i>
|
<a href="/dpanel/dashboard/admin/" class="btn btn-secondary btn-full">
|
||||||
|
<i class="fas fa-arrow-left"></i>
|
||||||
Retourner au dashboard admin
|
Retourner au dashboard admin
|
||||||
</a>
|
</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 %>">×</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
@@ -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 -->
|
||||||
|
|||||||
Reference in New Issue
Block a user