From de8c5ccb84371270880bf53600ad01e1f6b3d5b9 Mon Sep 17 00:00:00 2001 From: Dinawo Date: Sat, 14 Jun 2025 22:01:39 +0200 Subject: [PATCH] Update v1.1.1-beta1 --- ...discordWebhookSuspisiousAlertMiddleware.js | 35 +- Middlewares/watcherMiddleware.js | 44 +- models/banModel.js | 18 +- models/websocketManager.js | 90 + package-lock.json | 725 +++--- package.json | 5 +- public/assets/homelab_logo@2x.png | Bin 29156 -> 0 bytes public/assets/homelab_logo@3x.png | Bin 55004 -> 0 bytes public/css/dashboard.styles.css | 1611 ++++++++++++ public/js/dashboard-old.js | 2169 +++++++++++++++++ public/js/dashboard.js | 1974 +++++++++------ public/js/profile.script.js | 268 ++ routes/Dpanel/API/Collaboration.js | 358 +++ routes/Dpanel/API/MoveFile.js | 49 +- routes/Dpanel/API/RenameFolder.js | 205 ++ routes/Dpanel/API/RevokeToken.js | 64 + routes/Dpanel/API/UserSearch.js | 98 + routes/Dpanel/Dashboard/index.js | 161 +- routes/Dpanel/Folder/index.js | 84 + routes/routes.js | 12 +- server.js | 10 +- views/dashboard.ejs | 330 ++- views/profile.ejs | 1014 +++++++- views/promote.ejs | 5 +- 24 files changed, 8037 insertions(+), 1292 deletions(-) create mode 100644 models/websocketManager.js delete mode 100644 public/assets/homelab_logo@2x.png delete mode 100644 public/assets/homelab_logo@3x.png create mode 100644 public/js/dashboard-old.js create mode 100644 public/js/profile.script.js create mode 100644 routes/Dpanel/API/Collaboration.js create mode 100644 routes/Dpanel/API/RenameFolder.js create mode 100644 routes/Dpanel/API/RevokeToken.js create mode 100644 routes/Dpanel/API/UserSearch.js diff --git a/Middlewares/discordWebhookSuspisiousAlertMiddleware.js b/Middlewares/discordWebhookSuspisiousAlertMiddleware.js index 8ecac13..86bf711 100644 --- a/Middlewares/discordWebhookSuspisiousAlertMiddleware.js +++ b/Middlewares/discordWebhookSuspisiousAlertMiddleware.js @@ -32,9 +32,42 @@ function sendDiscordWebhook(url, req, statusCode) { const allowedIps = setupData[0].allowedIps || []; const ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress; + // Skip monitoring for localhost/local IPs + const localIps = ['127.0.0.1', '::1', 'localhost', '::ffff:127.0.0.1']; + if (localIps.includes(ip)) { + return; + } + + // Skip monitoring for Chrome DevTools requests + if (req.originalUrl.includes('.well-known/appspecific/com.chrome.devtools.json')) { + return; + } + + // Skip monitoring for legitimate API endpoints + const legitimateEndpoints = [ + '/api/dpanel/dashboard/profilpicture', + '/api/dpanel/dashboard/backgroundcustom', + '/api/dpanel/collaboration', + '/api/dpanel/users/search', + '/dpanel/dashboard/profil', + '/build-metadata', + '/api/dpanel/collaboration/add', + '/api/dpanel/collaboration/remove', + '/api/dpanel/collaboration/users' + ]; + + if (legitimateEndpoints.some(endpoint => req.originalUrl.includes(endpoint))) { + return; + } + + // Skip monitoring if IP is allowed if (isIpAllowed(ip, allowedIps)) { return; - } else { + } + + // Skip monitoring for authenticated users on dashboard routes + if (req.user && req.originalUrl.startsWith('/dpanel/dashboard')) { + return; } const fullUrl = `${req.protocol}://${req.get('host')}${req.originalUrl}`; diff --git a/Middlewares/watcherMiddleware.js b/Middlewares/watcherMiddleware.js index 70ac5b6..5b4908c 100644 --- a/Middlewares/watcherMiddleware.js +++ b/Middlewares/watcherMiddleware.js @@ -3,49 +3,65 @@ const chokidar = require('chokidar'); const fs = require('fs'); const { logger, ErrorLogger, logRequestInfo } = require('../config/logs'); +// Define file paths const userFilePath = path.resolve(__dirname, '../data/user.json'); const setupFilePath = path.resolve(__dirname, '../data/setup.json'); +const collaborationFilePath = path.resolve(__dirname, '../data/collaboration.json'); -let userData, setupData; +// Initialize data objects +let userData, setupData, collaborationData; +// Load initial user data try { userData = JSON.parse(fs.readFileSync(userFilePath, 'utf-8')); } catch (error) { ErrorLogger.error(`Error parsing user.json: ${error}`); } +// Load initial setup data try { setupData = JSON.parse(fs.readFileSync(setupFilePath, 'utf-8')); } catch (error) { ErrorLogger.error(`Error parsing setup.json: ${error}`); } -const watcher = chokidar.watch([userFilePath, setupFilePath], { +// Load initial collaboration data +try { + collaborationData = JSON.parse(fs.readFileSync(collaborationFilePath, 'utf-8')); +} catch (error) { + ErrorLogger.error(`Error parsing collaboration.json: ${error}`); +} + +// Set up file watcher +const watcher = chokidar.watch([userFilePath, setupFilePath, collaborationFilePath], { persistent: true }); +// Handle file changes watcher.on('change', (filePath) => { let modifiedFile; - if (filePath === userFilePath) { - try { + + try { + if (filePath === userFilePath) { userData = JSON.parse(fs.readFileSync(filePath, 'utf-8')); modifiedFile = 'user.json'; - } catch (error) { - logger.error(`Error parsing user.json: ${error}`); - } - } else if (filePath === setupFilePath) { - try { + } else if (filePath === setupFilePath) { setupData = JSON.parse(fs.readFileSync(filePath, 'utf-8')); modifiedFile = 'setup.json'; - } catch (error) { - logger.error(`Error parsing setup.json: ${error}`); + } else if (filePath === collaborationFilePath) { + collaborationData = JSON.parse(fs.readFileSync(filePath, 'utf-8')); + modifiedFile = 'collaboration.json'; } + + logger.info(`File ${modifiedFile} has been modified`); + } catch (error) { + ErrorLogger.error(`Error parsing ${modifiedFile}: ${error}`); } - - logger.info(`File ${modifiedFile} has been modified`); }); +// Export data access functions module.exports = { getUserData: () => Promise.resolve(userData), - getSetupData: () => Promise.resolve(setupData) + getSetupData: () => Promise.resolve(setupData), + getCollaborationData: () => Promise.resolve(collaborationData) }; \ No newline at end of file diff --git a/models/banModel.js b/models/banModel.js index aa2d92c..0dfc93f 100644 --- a/models/banModel.js +++ b/models/banModel.js @@ -10,10 +10,24 @@ const logAndBanSuspiciousActivity = async (req, res, next) => { const ip = req.headers['cf-connecting-ip'] || req.headers['x-forwarded-for'] || req.connection.remoteAddress; const url = `${req.protocol}://${req.get('host')}${req.originalUrl}`; -if (req.originalUrl === '/auth/activedirectory', "/favicon.ico" && req.method === 'POST') { + // Skip monitoring for localhost/local IPs + const localIps = ['127.0.0.1', '::1', 'localhost', '::ffff:127.0.0.1']; + if (localIps.includes(ip)) { next(); return; -} + } + + // Skip monitoring for Chrome DevTools requests + if (req.originalUrl.includes('.well-known/appspecific/com.chrome.devtools.json')) { + next(); + return; + } + + // Skip monitoring for specific endpoints + if (req.originalUrl === '/auth/activedirectory' || req.originalUrl === '/favicon.ico') { + next(); + return; + } let bans; try { diff --git a/models/websocketManager.js b/models/websocketManager.js new file mode 100644 index 0000000..f2883e3 --- /dev/null +++ b/models/websocketManager.js @@ -0,0 +1,90 @@ +const WebSocket = require('ws'); +const { logger } = require('../config/logs'); + +class WebSocketManager { + constructor(server) { + this.wss = new WebSocket.Server({ server }); + this.connections = new Map(); // Pour stocker les connexions utilisateur + this.init(); + } + + init() { + this.wss.on('connection', (ws, req) => { + + ws.on('message', (message) => { + try { + const data = JSON.parse(message); + this.handleMessage(ws, data); + } catch (error) { + logger.error('Error handling WebSocket message:', error); + } + }); + + ws.on('close', () => { + this.handleDisconnect(ws); + }); + }); + } + + handleMessage(ws, data) { + switch (data.type) { + case 'join': + this.handleJoin(ws, data); + break; + case 'leave': + this.handleLeave(ws, data); + break; + default: + logger.warn('Unknown message type:', data.type); + } + } + + handleJoin(ws, data) { + const { userId, fileId } = data; + this.connections.set(ws, { userId, fileId }); + this.broadcastFileStatus(fileId); + } + + handleLeave(ws, data) { + const { fileId } = data; + this.connections.delete(ws); + this.broadcastFileStatus(fileId); + } + + handleDisconnect(ws) { + const connection = this.connections.get(ws); + if (connection) { + this.broadcastFileStatus(connection.fileId); + this.connections.delete(ws); + } + } + + broadcastFileStatus(fileId) { + const activeUsers = Array.from(this.connections.values()) + .filter(conn => conn.fileId === fileId) + .map(conn => conn.userId); + + const message = JSON.stringify({ + type: 'fileStatus', + fileId, + activeUsers + }); + + this.wss.clients.forEach(client => { + if (client.readyState === WebSocket.OPEN) { + client.send(message); + } + }); + } + + // Méthode pour envoyer une mise à jour à tous les clients + broadcast(data) { + this.wss.clients.forEach(client => { + if (client.readyState === WebSocket.OPEN) { + client.send(JSON.stringify(data)); + } + }); + } +} + +module.exports = WebSocketManager; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 52d567e..8789650 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@cdn-app/insider-myaxrin-labs-dinawo", - "version": "1.1.0-beta.1", + "version": "1.1.1-beta.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@cdn-app/insider-myaxrin-labs-dinawo", - "version": "1.1.0-beta.1", + "version": "1.1.1-beta.1", "license": "ISC", "dependencies": { "@auth/express": "^0.5.1", @@ -62,7 +62,8 @@ "tailwindcss": "^3.3.5", "toastify-js": "^1.12.0", "winston": "^3.11.0", - "winston-daily-rotate-file": "^4.7.1" + "winston-daily-rotate-file": "^4.7.1", + "ws": "^8.18.0" }, "devDependencies": { "daisyui": "^4.5.0", @@ -433,9 +434,9 @@ "license": "MIT" }, "node_modules/@types/cors": { - "version": "2.8.17", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", - "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", "license": "MIT", "dependencies": { "@types/node": "*" @@ -454,12 +455,12 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.10.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", - "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==", + "version": "24.0.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.1.tgz", + "integrity": "sha512-MX4Zioh39chHlDJbKmEgydJDS3tspMP/lnQC67G3SWsTnb9NeYVWOjkxpOSy4oMfPs4StcWHwBrvUb4ybfnuaw==", "license": "MIT", "dependencies": { - "undici-types": "~6.20.0" + "undici-types": "~7.8.0" } }, "node_modules/@types/triple-beam": { @@ -521,6 +522,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/activedirectory2/-/activedirectory2-2.2.0.tgz", "integrity": "sha512-uGbw74xttFG6hgocU8T1a0oDofLsyTp44BPTn42JN5C2QlyO5kRl2E7ZoUdfpFzV+yxhaQTKI+8QqRB5HONYvA==", + "deprecated": "Decomissioned.", "license": "MIT", "dependencies": { "abstract-logging": "^2.0.0", @@ -797,12 +799,12 @@ } }, "node_modules/assert-options": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/assert-options/-/assert-options-0.8.2.tgz", - "integrity": "sha512-XaXoMxY0zuwAb0YuZjxIm8FeWvNq0aWNIbrzHhFjme8Smxw4JlPoyrAKQ6808k5UvQdhvnWqHZCphq5mXd4TDA==", + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/assert-options/-/assert-options-0.8.3.tgz", + "integrity": "sha512-s6v4HnA+vYSGO4eZX+F+I3gvF74wPk+m6Z1Q3w1Dsg4Pnv/R24vhKAasoMVZGvDpOOfTg1Qz4ptZnEbuy95XsQ==", "license": "MIT", "engines": { - "node": ">=10.0.0" + "node": ">=14.0.0" } }, "node_modules/assert-plus": { @@ -836,9 +838,9 @@ } }, "node_modules/axios": { - "version": "1.7.9", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", - "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz", + "integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -1021,9 +1023,9 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -1119,28 +1121,10 @@ "node": ">=14.16" } }, - "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/call-bind-apply-helpers": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", - "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -1151,13 +1135,13 @@ } }, "node_modules/call-bound": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.2.tgz", - "integrity": "sha512-0lk0PHFe/uz0vl527fG9CgdE9WdafjDbCXvBbs+LUv000TVt2Jjhqbs4Jwm8gz070w8xXyEAxrPOMullsxXeGg==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "get-intrinsic": "^1.2.5" + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" }, "engines": { "node": ">= 0.4" @@ -1351,9 +1335,9 @@ } }, "node_modules/compression": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.5.tgz", - "integrity": "sha512-bQJ0YRck5ak3LgtnpKkiabX5pNF7tMUh1BSy2ZBOTh0Dim0BUu6aPPwByIns6/A5Prh8PufSPerMDUklpzes2Q==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.0.tgz", + "integrity": "sha512-k6WLKfunuqCYD3t6AsuPGvQWaKwuLLh2/xHNcX4qE+vIfDNXpSqnrhwA7O53R7WVQUnt8dVAIW+YHr7xTgOgGA==", "license": "MIT", "dependencies": { "bytes": "3.1.2", @@ -1543,9 +1527,9 @@ } }, "node_modules/daisyui": { - "version": "4.12.22", - "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-4.12.22.tgz", - "integrity": "sha512-HDLWbmTnXxhE1MrMgSWjVgdRt+bVYHvfNbW3GTsyIokRSqTHonUTrxV3RhpPDjGIWaHt+ELtDCTYCtUFgL2/Nw==", + "version": "4.12.24", + "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-4.12.24.tgz", + "integrity": "sha512-JYg9fhQHOfXyLadrBrEqCDM6D5dWCSSiM6eTNCRrBRzx/VlOCrLS8eDfIw9RVvs64v2mJdLooKXY8EwQzoszAA==", "dev": true, "license": "MIT", "dependencies": { @@ -1575,9 +1559,9 @@ } }, "node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -1627,23 +1611,6 @@ "node": ">=10" } }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -1688,9 +1655,9 @@ } }, "node_modules/detect-libc": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", - "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", "license": "Apache-2.0", "engines": { "node": ">=8" @@ -1754,9 +1721,9 @@ } }, "node_modules/dotenv": { - "version": "16.4.7", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", - "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -1780,12 +1747,12 @@ } }, "node_modules/dunder-proto": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.0.tgz", - "integrity": "sha512-9+Sj30DIu+4KvHqMfLUGLFYL2PkURSYMVXJyXe92nFRvlYq5hBjLEhblKB+vkd/WVlUYMWigiY07T91Fkk0+4A==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.0", + "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" }, @@ -1851,12 +1818,11 @@ } }, "node_modules/engine.io": { - "version": "6.6.2", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz", - "integrity": "sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==", + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", + "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==", "license": "MIT", "dependencies": { - "@types/cookie": "^0.4.1", "@types/cors": "^2.8.12", "@types/node": ">=10.0.0", "accepts": "~1.3.4", @@ -1880,12 +1846,6 @@ "node": ">=10.0.0" } }, - "node_modules/engine.io/node_modules/@types/cookie": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", - "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==", - "license": "MIT" - }, "node_modules/engine.io/node_modules/cookie": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", @@ -1912,6 +1872,27 @@ } } }, + "node_modules/engine.io/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/entities": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", @@ -1940,9 +1921,9 @@ } }, "node_modules/es-object-atoms": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", - "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -1951,6 +1932,21 @@ "node": ">= 0.4" } }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -2052,9 +2048,9 @@ "license": "MIT" }, "node_modules/express-rate-limit": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.4.1.tgz", - "integrity": "sha512-KS3efpnpIDVIXopMc65EMbWbUht7qvTCdtCR2dD/IZmi9MIkopYESwyRqLgv8Pfu589+KqDqOdzJWW7AHoACeg==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", + "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", "license": "MIT", "engines": { "node": ">= 16" @@ -2063,7 +2059,7 @@ "url": "https://github.com/sponsors/express-rate-limit" }, "peerDependencies": { - "express": "4 || 5 || ^5.0.0-beta.1" + "express": "^4.11 || 5 || ^5.0.0-beta.1" } }, "node_modules/express-session": { @@ -2149,16 +2145,16 @@ "license": "MIT" }, "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", - "micromatch": "^4.0.4" + "micromatch": "^4.0.8" }, "engines": { "node": ">=8.6.0" @@ -2172,9 +2168,9 @@ "license": "MIT" }, "node_modules/fastq": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", - "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", "license": "ISC", "dependencies": { "reusify": "^1.0.4" @@ -2205,9 +2201,9 @@ } }, "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -2297,12 +2293,12 @@ } }, "node_modules/foreground-child": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", - "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", "license": "ISC", "dependencies": { - "cross-spawn": "^7.0.0", + "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" }, "engines": { @@ -2325,13 +2321,15 @@ } }, "node_modules/form-data": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", - "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz", + "integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { @@ -2372,9 +2370,9 @@ "license": "ISC" }, "node_modules/fs-extra": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", - "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", + "integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==", "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", @@ -2491,21 +2489,21 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.6.tgz", - "integrity": "sha512-qxsEs+9A+u85HhllWJJFicJfPDhRmjzoYdl64aMWW9yRIJmSyxdn8IEkuIM530/7T+lv0TIHd8L6Q/ra0tEoeA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "dunder-proto": "^1.0.0", + "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", + "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", - "math-intrinsics": "^1.0.0" + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -2514,6 +2512,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -2632,18 +2643,6 @@ "node": ">=8" } }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -2656,6 +2655,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-unicode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", @@ -2675,9 +2689,9 @@ } }, "node_modules/http-cache-semantics": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", - "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", "license": "BSD-2-Clause" }, "node_modules/http-errors": { @@ -2816,9 +2830,9 @@ } }, "node_modules/is-core-module": { - "version": "2.16.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.0.tgz", - "integrity": "sha512-urTSINYfAYgcbLb0yDQ6egFm6h3Mo1DcF9EkyXSRjjzdHbsulg01qhwWuXdOoUBuTkbQ80KDboXa0vFJ+BDH+g==", + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -2957,18 +2971,18 @@ } }, "node_modules/jiti": { - "version": "1.21.6", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz", - "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==", + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "license": "MIT", "bin": { "jiti": "bin/jiti.js" } }, "node_modules/jose": { - "version": "5.9.6", - "resolved": "https://registry.npmjs.org/jose/-/jose-5.9.6.tgz", - "integrity": "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ==", + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz", + "integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/panva" @@ -3027,12 +3041,12 @@ } }, "node_modules/jwa": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", - "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", "license": "MIT", "dependencies": { - "buffer-equal-constant-time": "1.0.1", + "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } @@ -3116,6 +3130,7 @@ "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", "license": "MIT" }, "node_modules/lodash.includes": { @@ -3134,6 +3149,7 @@ "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", "license": "MIT" }, "node_modules/lodash.isinteger": { @@ -3196,9 +3212,9 @@ } }, "node_modules/long": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", - "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", "license": "Apache-2.0" }, "node_modules/lowercase-keys": { @@ -3223,9 +3239,9 @@ } }, "node_modules/lru.min": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.1.tgz", - "integrity": "sha512-FbAj6lXil6t8z4z3j0E5mfRlPzxkySotzUHwRXjlpRh10vc6AI6WN62ehZj82VG7M20rqogJ0GLwar2Xa05a8Q==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.2.tgz", + "integrity": "sha512-Nv9KddBcQSlQopmBHXSsZVY5xsdlZkdH/Iey0BlcBYggMd4two7cZnKOK9vmy3nY0O5RGH99z1PCeTpPqszUYg==", "license": "MIT", "engines": { "bun": ">=1.0.0", @@ -3262,9 +3278,9 @@ } }, "node_modules/math-intrinsics": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.0.0.tgz", - "integrity": "sha512-4MqMiKP90ybymYvsut0CH2g4XWbfLtmlCkXmtmdcDCxNB+mQcu1w/1+L/VD7vi/PSv7X2JYV7SCcR+jiPXnQtA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -3344,9 +3360,9 @@ } }, "node_modules/mime-db": { - "version": "1.53.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.53.0.tgz", - "integrity": "sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg==", + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -3468,9 +3484,10 @@ "license": "MIT" }, "node_modules/multer": { - "version": "1.4.5-lts.1", - "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz", - "integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==", + "version": "1.4.5-lts.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz", + "integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==", + "deprecated": "Multer 1.x is impacted by a number of vulnerabilities, which have been patched in 2.x. You should upgrade to the latest 2.x version.", "license": "MIT", "dependencies": { "append-field": "^1.0.0", @@ -3581,9 +3598,9 @@ } }, "node_modules/mysql2": { - "version": "3.11.5", - "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.11.5.tgz", - "integrity": "sha512-0XFu8rUmFN9vC0ME36iBvCUObftiMHItrYFhlCRvFWbLgpNqtC4Br/NmZX1HNCszxT0GGy5QtP+k3Q3eCJPaYA==", + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.14.1.tgz", + "integrity": "sha512-7ytuPQJjQB8TNAYX/H2yhL+iQOnIBjAMam361R7UAL0lOVXWjtdrmoL9HYKqKoLp/8UUTRcvo1QPvK9KL7wA8w==", "license": "MIT", "dependencies": { "aws-ssl-profiles": "^1.1.1", @@ -3636,16 +3653,16 @@ } }, "node_modules/nan": { - "version": "2.22.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz", - "integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==", + "version": "2.22.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.2.tgz", + "integrity": "sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ==", "license": "MIT", "optional": true }, "node_modules/nanoid": { - "version": "3.3.8", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", - "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "funding": [ { "type": "github", @@ -3726,18 +3743,18 @@ } }, "node_modules/nodemailer": { - "version": "6.9.16", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.16.tgz", - "integrity": "sha512-psAuZdTIRN08HKVd/E8ObdV6NO7NTBY3KsC30F7M4H1OnmLCUNaS56FpYxyb26zWLSyYF9Ozch9KYHhHegsiOQ==", + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.1.tgz", + "integrity": "sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==", "license": "MIT-0", "engines": { "node": ">=6.0.0" } }, "node_modules/nodemon": { - "version": "3.1.9", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.9.tgz", - "integrity": "sha512-hdr1oIb2p6ZSxu3PB2JWWYS7ZQ0qvaZsc3hK8DR8f02kRzc8rjYmxAIvdz+aYC+8F2IjNaB7HMcSDg8nQpJxyg==", + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", + "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==", "dev": true, "license": "MIT", "dependencies": { @@ -3811,9 +3828,9 @@ } }, "node_modules/normalize-url": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.1.tgz", - "integrity": "sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w==", + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.2.tgz", + "integrity": "sha512-Ee/R3SyN4BuynXcnTaekmaVdbDAEiNrHqjQIA37mHU8G9pf7aaAD4ZX3XjBLo6rsdcxA/gtkcNYZLt30ACgynw==", "license": "MIT", "engines": { "node": ">=14.16" @@ -3836,9 +3853,9 @@ } }, "node_modules/oauth": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.0.tgz", - "integrity": "sha512-1orQ9MT1vHFGQxhuy7E/0gECD3fd2fCC+PIX+/jgmU/gI3EpRocXtmtvxCO5x3WZ443FLTLFWNDjl5MPJf9u+Q==", + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.2.tgz", + "integrity": "sha512-JtFnB+8nxDEXgNyniwz573xxbKSOu3R8D40xQKqcjwJ2CDkYqUDI53o6IuzDJBx60Z8VKCm271+t8iFjakrl8Q==", "license": "MIT" }, "node_modules/oauth4webapi": { @@ -3869,9 +3886,9 @@ } }, "node_modules/object-inspect": { - "version": "1.13.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", - "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==", + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -4127,14 +4144,109 @@ "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" }, "node_modules/pg": { - "version": "8.13.1", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.13.1.tgz", - "integrity": "sha512-OUir1A0rPNZlX//c7ksiu7crsGZTKSOXJPgtNiHGIlC9H0lO+NC6ZDYksSgBYY/thSWhnSRBv8w1lieNNGATNQ==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.0.tgz", + "integrity": "sha512-7SKfdvP8CTNXjMUzfcVTaI+TDzBEeaUnVwiVGZQD1Hh33Kpev7liQba9uLd4CfN8r9mCVsD0JIpq03+Unpz+kg==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.9.0", + "pg-pool": "^3.10.0", + "pg-protocol": "^1.10.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 8.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.2.5" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.5.tgz", + "integrity": "sha512-OOX22Vt0vOSRrdoUPKJ8Wi2OpE/o/h9T8X1s4qSkCedbNah9ei2W2765be8iMVxQUsvgT7zIAT2eIa9fs5+vtg==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.0.tgz", + "integrity": "sha512-P2DEBKuvh5RClafLngkAuGe9OUlFV7ebu8w1kmaaOgPcpJd1RIFh7otETfI6hAR8YupOLFTY7nuvvIn7PLciUQ==", + "license": "MIT" + }, + "node_modules/pg-cursor": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/pg-cursor/-/pg-cursor-2.15.0.tgz", + "integrity": "sha512-sO3SQP9seXoaV7sddeKrPQk3zhnrMchCr71cYjkyNHCC6n3mA5AAyhK5foxy+yBtnBZuPqxzoiVxz3jKyV7D3g==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "pg": "^8" + } + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-minify": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/pg-minify/-/pg-minify-1.7.0.tgz", + "integrity": "sha512-kFPxAWAhPMvOqnY7klP3scdU5R7bxpAYOm8vGExuIkcSIwuFkZYl4C4XIPQ8DtXY2NzVmAX1aFHpvFSXQ/qQmA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.0.tgz", + "integrity": "sha512-DzZ26On4sQ0KmqnO34muPcmKbhrjmyiO4lCCR0VwEd7MjmiKf5NTg/6+apUEu0NF7ESa37CGzFxH513CoUmWnA==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-promise": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/pg-promise/-/pg-promise-11.14.0.tgz", + "integrity": "sha512-x/HZ6hK0MxYllyfUbmN/XZc7JBYoow7KElyNW9hnlhgRHMiRZmRUtfNM/wcuElpjSoASPxkoIKi4IA5QlwOONA==", + "license": "MIT", + "dependencies": { + "assert-options": "0.8.3", + "pg": "8.14.1", + "pg-minify": "1.7.0", + "spex": "3.4.1" + }, + "engines": { + "node": ">=14.0" + }, + "peerDependencies": { + "pg-query-stream": "4.8.1" + } + }, + "node_modules/pg-promise/node_modules/pg": { + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.14.1.tgz", + "integrity": "sha512-0TdbqfjwIun9Fm/r89oB7RFQ0bLgduAhiIqIXOsyKoiC/L54DbuAAzIEN/9Op0f1Po9X7iCPXGoa/Ah+2aI8Xw==", "license": "MIT", "dependencies": { "pg-connection-string": "^2.7.0", - "pg-pool": "^3.7.0", - "pg-protocol": "^1.7.0", + "pg-pool": "^3.8.0", + "pg-protocol": "^1.8.0", "pg-types": "^2.1.0", "pgpass": "1.x" }, @@ -4153,88 +4265,20 @@ } } }, - "node_modules/pg-cloudflare": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz", - "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==", - "license": "MIT", - "optional": true - }, - "node_modules/pg-connection-string": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.7.0.tgz", - "integrity": "sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==", - "license": "MIT" - }, - "node_modules/pg-cursor": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/pg-cursor/-/pg-cursor-2.12.1.tgz", - "integrity": "sha512-V13tEaA9Oq1w+V6Q3UBIB/blxJrwbbr35/dY54r/86soBJ7xkP236bXaORUTVXUPt9B6Ql2BQu+uwQiuMfRVgg==", - "license": "MIT", - "peer": true, - "peerDependencies": { - "pg": "^8" - } - }, - "node_modules/pg-int8": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", - "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", - "license": "ISC", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/pg-minify": { - "version": "1.6.5", - "resolved": "https://registry.npmjs.org/pg-minify/-/pg-minify-1.6.5.tgz", - "integrity": "sha512-u0UE8veaCnMfJmoklqneeBBopOAPG3/6DHqGVHYAhz8DkJXh9dnjPlz25fRxn4e+6XVzdOp7kau63Rp52fZ3WQ==", - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/pg-pool": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.7.0.tgz", - "integrity": "sha512-ZOBQForurqh4zZWjrgSwwAtzJ7QiRX0ovFkZr2klsen3Nm0aoh33Ls0fzfv3imeH/nw/O27cjdz5kzYJfeGp/g==", - "license": "MIT", - "peerDependencies": { - "pg": ">=8.0" - } - }, - "node_modules/pg-promise": { - "version": "11.10.2", - "resolved": "https://registry.npmjs.org/pg-promise/-/pg-promise-11.10.2.tgz", - "integrity": "sha512-wK4yjxZdfxBmAMcs40q6IsC1SOzdLilc1yNvJqlbOjtm2syayqLDCt1JQ9lhS6yNSgVlGOQZT88yb/SADJmEBw==", - "license": "MIT", - "dependencies": { - "assert-options": "0.8.2", - "pg": "8.13.1", - "pg-minify": "1.6.5", - "spex": "3.4.0" - }, - "engines": { - "node": ">=14.0" - }, - "peerDependencies": { - "pg-query-stream": "4.7.1" - } - }, "node_modules/pg-protocol": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.7.0.tgz", - "integrity": "sha512-hTK/mE36i8fDDhgDFjy6xNOG+LCorxLG3WO17tku+ij6sVHXh1jQUJ8hYAnRhNla4QVD2H8er/FOjc/+EgC6yQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.0.tgz", + "integrity": "sha512-IpdytjudNuLv8nhlHs/UrVBhU0e78J0oIS/0AVdTbWxSOkFUVdsHC/NrorO6nXsQNDTT1kzDSOMJubBQviX18Q==", "license": "MIT" }, "node_modules/pg-query-stream": { - "version": "4.7.1", - "resolved": "https://registry.npmjs.org/pg-query-stream/-/pg-query-stream-4.7.1.tgz", - "integrity": "sha512-UMgsgn/pOIYsIifRySp59vwlpTpLADMK9HWJtq5ff0Z3MxBnPMGnCQeaQl5VuL+7ov4F96mSzIRIcz+Duo6OiQ==", + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/pg-query-stream/-/pg-query-stream-4.8.1.tgz", + "integrity": "sha512-kZo6C6HSzYFF6mlwl+etDk5QZD9CMdlHUXpof6PkK9+CHHaBLvOd2lZMwErOOpC/ldg4thrAojS8sG1B8PZ9Yw==", "license": "MIT", "peer": true, "dependencies": { - "pg-cursor": "^2.12.1" + "pg-cursor": "^2.13.1" }, "peerDependencies": { "pg": "^8" @@ -4293,18 +4337,18 @@ } }, "node_modules/pirates": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", - "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", "license": "MIT", "engines": { "node": ">= 6" } }, "node_modules/postcss": { - "version": "8.4.49", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", - "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", + "version": "8.5.5", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.5.tgz", + "integrity": "sha512-d/jtm+rdNT8tpXuHY5MMtcbJFBkhXE6593XVR9UoGCH8jSFGci7jGvMGH5RYd5PBJW+00NZQt6gf7CbagJCrhg==", "funding": [ { "type": "opencollective", @@ -4321,7 +4365,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.7", + "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -4401,15 +4445,15 @@ } }, "node_modules/postcss-load-config/node_modules/yaml": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.1.tgz", - "integrity": "sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", + "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", "license": "ISC", "bin": { "yaml": "bin.mjs" }, "engines": { - "node": ">= 14" + "node": ">= 14.6" } }, "node_modules/postcss-nested": { @@ -4722,9 +4766,9 @@ } }, "node_modules/resolve": { - "version": "1.22.9", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.9.tgz", - "integrity": "sha512-QxrmX1DzraFIi9PxdG5VkRfRwIgjwyud+z/iBwfRRrVmHc+P9Q7u2lSSpQ6bjr2gy5lrqIiU9vb6iAeGf2400A==", + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", "license": "MIT", "dependencies": { "is-core-module": "^2.16.0", @@ -4734,6 +4778,9 @@ "bin": { "resolve": "bin/resolve" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -4760,9 +4807,9 @@ } }, "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", "license": "MIT", "engines": { "iojs": ">=1.0.0", @@ -4851,9 +4898,9 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -4936,23 +4983,6 @@ "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", "license": "ISC" }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -5134,6 +5164,27 @@ } } }, + "node_modules/socket.io-adapter/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/socket.io-parser": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", @@ -5191,9 +5242,9 @@ } }, "node_modules/spex": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/spex/-/spex-3.4.0.tgz", - "integrity": "sha512-8JeZJ7QlEBnSj1W1fKXgbB2KUPA8k4BxFMf6lZX/c1ZagU/1b9uZWZK0yD6yjfzqAIuTNG4YlRmtMpQiXuohsg==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/spex/-/spex-3.4.1.tgz", + "integrity": "sha512-Br0Mu3S+c70kr4keXF+6K4B8ohR+aJjI9s7SbdsI3hliE1Riz4z+FQk7FQL+r7X1t90KPkpuKwQyITpCIQN9mg==", "license": "MIT", "engines": { "node": ">=14.0.0" @@ -5335,9 +5386,9 @@ } }, "node_modules/sucrase/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -5474,9 +5525,9 @@ } }, "node_modules/swagger-ui-dist": { - "version": "5.18.2", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.18.2.tgz", - "integrity": "sha512-J+y4mCw/zXh1FOj5wGJvnAajq6XgHOyywsa9yITmwxIlJbMqITq3gYRZHaeqLVH/eV/HOPphE6NjF+nbSNC5Zw==", + "version": "5.24.1", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.24.1.tgz", + "integrity": "sha512-ITeWc7CCAfK53u8jnV39UNqStQZjSt+bVYtJHsOEL3vVj/WV9/8HmsF8Ej4oD8r+Xk1HpWyeW/t59r1QNeAcUQ==", "license": "Apache-2.0", "dependencies": { "@scarf/scarf": "=1.4.0" @@ -5498,9 +5549,9 @@ } }, "node_modules/sweetalert2": { - "version": "11.15.0", - "resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.15.0.tgz", - "integrity": "sha512-34Xs0CFBac6I1cGG9d+XaBqJrp0F/0prr8rMYOcU0shU/XmkGkRtlCxWNi7PdKYGw9Qf6aoEHNYicX3au37nkw==", + "version": "11.22.0", + "resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.22.0.tgz", + "integrity": "sha512-pSMuRGDULhh+wrFkO22O0YsIXxs8yFE0O+WVYXcqc/sTa1oRnf0JlR+vfQIRY1QM1UeFfnCjyw6DYnG75/oxiQ==", "license": "MIT", "funding": { "type": "individual", @@ -5508,9 +5559,9 @@ } }, "node_modules/systeminformation": { - "version": "5.23.13", - "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.23.13.tgz", - "integrity": "sha512-4Cn39sTXp7eN9rV/60aNdCXgpQ5xPcUUPIbwJjqTKj4/bNcSYtgZvFdXvZj6pRHP4h17uk6MfdMP5Fm6AK/b0Q==", + "version": "5.27.1", + "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.27.1.tgz", + "integrity": "sha512-FgkVpT6GgATtNvADgtEzDxI/SVaBisfnQ4fmgQZhCJ4335noTgt9q6O81ioHwzs9HgnJaaFSdHSEMIkneZ55iA==", "license": "MIT", "os": [ "darwin", @@ -5534,9 +5585,9 @@ } }, "node_modules/tailwindcss": { - "version": "3.4.16", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.16.tgz", - "integrity": "sha512-TI4Cyx7gDiZ6r44ewaJmt0o6BrMCT5aK5e0rmJ/G9Xq3w7CX/5VXl/zIPEJZFUK5VEqwByyhqNPycPlvcK4ZNw==", + "version": "3.4.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", + "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", @@ -5747,9 +5798,9 @@ "license": "MIT" }, "node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", "license": "MIT" }, "node_modules/universalify": { @@ -5810,9 +5861,9 @@ } }, "node_modules/validator": { - "version": "13.12.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", - "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", + "version": "13.15.15", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz", + "integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==", "license": "MIT", "engines": { "node": ">= 0.10" @@ -6114,9 +6165,9 @@ "license": "ISC" }, "node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", + "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", "license": "MIT", "engines": { "node": ">=10.0.0" diff --git a/package.json b/package.json index 8ce3f13..5e70a4e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@cdn-app/insider-myaxrin-labs-dinawo", - "version": "1.1.0-beta.1", + "version": "1.1.1-beta.1", "description": "", "main": "server.js", "scripts": { @@ -63,7 +63,8 @@ "tailwindcss": "^3.3.5", "toastify-js": "^1.12.0", "winston": "^3.11.0", - "winston-daily-rotate-file": "^4.7.1" + "winston-daily-rotate-file": "^4.7.1", + "ws": "^8.18.0" }, "devDependencies": { "daisyui": "^4.5.0", diff --git a/public/assets/homelab_logo@2x.png b/public/assets/homelab_logo@2x.png deleted file mode 100644 index 0dadd907c8e9292d210731b1639ef56a3997862a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 29156 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

t7d`VjSW5rocfUrPJ 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 CgIHMr diff --git a/public/assets/homelab_logo@3x.png b/public/assets/homelab_logo@3x.png deleted file mode 100644 index 0d2e462512918e7c058160886f46f66d682bf97b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 55004 zcmeEt1y5y7ur2OBxVyW%!vKTp0E4@`40;akHn_XHyA1B`c5rux0UqDIc|YPMFO{9{ z>~yEnyRxgQSFH+HR{V~PfR6wM28JvvBdH1o2JZ3S0|)c{ z0tQA3CMzkX?x}y?1)E9v-A&}9yu5?Pwzbl+Bn+z;_Ae9=O@%bDj~bWJwin9ij|v#0 zDOxU78s)d3aG0RJ6;h*Tllo_1RIpS66bApGz=`V%_r}%`{HET+XQ!6N=3)a!LkEqO zgF`jUxQ@c{b^DucP4|QE_Hb{H57hsP7c)fV(bPyzTrA*!1kBU%i3j*UQ&Cju-av0u zeRY31VTk{Pc;j6NMDd@@1fpMqwgPb$a}xe{AFPKz>NDJba&HJF*!l#LdMMfdpQWI_ z&27m4-7ka-j(RENsq#+u@;^(zR6YH#*8i{S|M3qv6N&D4bq3irob(O%=}JE;MWlo{ zsu#HREQE{TFBj?vrE|6SMi51Ed_nRz?Aw?9>VcHrzbdCn^#6!N{)wpi4SnItAb z;eL9W=-Y-9hKP&-z&=TZPJDt$CBd%dkyvZm4$}?K#Y(cg_Vm7;S*W?Iek(`a7O7BN zYw$lz9Sy1pb}ovQO2U95GYOEu4e8~Dq*jA-KcD$ajz`4^nxcT5-UE3OM-6D=VDd^Z94ats z0NuZw08v@0L~=N#V1T3}ST)>&Q={QC94mAJ#W4;wbZ!8B z**U*(KZZ1h$L?ptYjJdmx_!@HI?49+4UV7*s-1EV!ci?_!8vNV#h1ytELbkcyp1;q z`BYdWK0gBcytdWWtU0rq_<4vdDkou2og@*nEdOM2jvlvZh zijs|YnJ-s2ectq#SVY}uOs{cFPNiB0p_Z$|Y6cy!7s{0<%7OxnFi=$K6`D!cc4vCg ze5j!0#d!3Xolt~SO2h9*3)opZEtG+KfK^9fmc>LRG`Mlrdc;qL&dU?)nGfR02 z_jEMaT0U~*W$Phcg#a5@FCRfHNbDl{3rYvX% zcMe1f<0ji3RHjY!WdH(lz(kQUe~bAzTmfll>(vkwt@9gs%t=i z&nS|KU0XnqBEf;q6gId4a8Qm^>R@z1)-a@)m2~8#RP06x7etuXsmX3F_H^P#hjj4hH<7myIcCSB@4a03iV6zCBK8b%XLSUdDpe;Te2FqNv8v zxHDa2u6NGFtHlLV#piXzNq|ACJ|UEQeF$LupmV$igTWZ}#%860%4Mcep`&d0^QI_d zzUXE%XK@>P#?<3_bkXN~VtnzmKn-!E=l9#{q5tFb-oUQvb5f?k z$9-&UIP}07@3(y!YmH)3T(4ZztsR6};rBRtBSC0zWF1VY{GddK}mH|`i-Mzr%2 z53sVfJ!t#zL!=15$*e*37_gYyDJ`Q#^+aM+AeAdRjiqNSD-Rb|6I1J9y(VsN`v}F}*MdWGotfD8qRQRnJ{b7#(%zrn6B8J$O8^3ohWNH9j z@(1Z}Xbk;;MCdrER$28f8QL;jdMkN=E~;`%zyhK%_Z$m%|MzRIMtC4^0JHgJY5)an zO@oW7C`Rp`@J$PT?E_(78gj09NOwh8&u~=EA*AG)CA#n&clxC)xFm#X?mty=`OYpt zL@8JXbSWTgoV7y$u-1es;5XUA<9pe!<$uzTV}hMCtNwv~i{4uEl>qq}P#pXB#KD=a zzLwN%zU6+dCU?4yUyi1zYZAG2VkTMEZ$UB3tje@I7mBpXiLpYH9ZX3ketU{04ZpH( z50U@~w$H8dDq|}_mkhM18DThIOO}wTo7g*)5l|$PnGcy>Hl{}8?DGcVPf!_!bX5J`KuFDoMWJ{bI{%+GEjyA(H4<#u}JBVk)tx}Fx?rdUyr(%4#P3$Lk zf3Qr{vBccUOj-OPAPk}Rr0Zfs`c;~78+=& z{sQ7KaKxz)E!osXzws8Ll|coG(+{ctxkbY${NV|JaD`Ky$OSI2=|C0WsKW2#1#}Mn zWXhndLIScmKc=^nMdJM!pZNyF6ExrUB1oJja8uAsQ3vflaJO0~2U&V%2xm7`{CFEFpOWf#iuN}3|b=z^WrHh<_{M7fEUHXf;%^D(3SEoP3VwFB~ z_6DeeUOgcQ>og7R>4mnKPF;AKNv{3!1{GET2~?E!}>ano2(c7CZaMmBj{BWq@qzordPs zFAn)C6KUEJlxn3vYiZ(`x*1Wc`|v+|wjvIeWSy+&l`mKxEKrmMNaeRju~nkQht6~G ztcte$gmG8I{*KWh21eoWHf{o&g-Az>g>i!C#xVvG6t1@G^TxWbNA?2z{ZHV}&W+EU zdzL<)la0P|bC&+dIml4S(w7*L&)0z)3gX+R0&fDAm4+Jk+pTyQ)W1peDr6?V&{*>e zl}V4~D8BC}U<=Xha9K-bRWXoSZ<&VYgeZ=9*oDxZuu!0Kh8s z;UUC+jujY>d{n$rI^|ZH(SELdvmFd()}&1&WCzK@p2-q%K7gGo5&cIZ8e8ewO0|ZV z&*BNetH@`$+S2x+khK>moENSH)b~yb)ivxP+bet@m=Uw;az6T5Yf|`_P}*ocS*)P@ zbB+;cNQEZIo{o))mf(qwn@bpyM~7rR01s@_hNJq!!g;~o>8%LlU5J*_Po&kk9~xXi zir1?u_)yHYIs(+RdA|MJUni%hCE&f!uPOGvozO!SY+E<&nAkMGV1o?{Cif0>w#GJW z3!;<(z{1zQ4h-bVb$^Co?R?+54>R$*;(I3cwII-rDVg3gb)nvFT<$yWG&fDL)9WK9 z($fI9sjLifYL}FG&B<;2u*d}kn<9Ey{7DA2S=@#?q=5!-nRV>_?G}OgF#-X-+>2c1 zY+2DC_`hbPF0&Z)X4isnVR;vEoQ6thinXA2Pt`O-N!mU4BDOsNB2hV&ki2fFm#}D@ zpMRRsNgMxxl&VGS$Ky*neb>96rQUrOMRD1=g=d4-?N`Ck86>DmMe%va;P~J$J9Bk1 zQ)bvg5P-N!VSj7k>-6JXtC8v@CXI2l-C*oroX1&{9jBQ5cm)yCG<8FwqMcDI@iUj05DB@1*V)mKG}DR=BY|aM-w_ z1MedQAZNfT8@y74>F7;47md|f6y0kY9Ebu0BbP00+q|U^2QHg8fu%CgNtAJ1Vrs0V z!D|-8KXQXi(TaH&PE19Ru?9wMO(1i+Z}T7({TOgg_*CfO>A~G7cNpzCHuIM?U~<-)0IA4Ah@k7ETPciY*YM4--J*u zy}C{#C7Q=c0=6~exYz8`E1E z8gbWO!9Xh5-|K{hiE^cdKaIxoupubPuuaHnIcRBg_=4)nLglF9jr-0=J}7=(Fev@( zIsH~6Xm+qk3_9wfLVaWLbQC#v0KO3elnlJ@%DtDfuiFhS(_0-kPfeDfITCo|R%wCV zyaBUJ1&&u;{b(T!fV|lPIH1u33IyL0OZ*=S=j)++s__jw?5N>V4hyE_I(i($^p~D> zre+K`V@3$ZB#F=F4jYYSUd8xy>@f|l*QX0i9wt{6v6DY)oNKN~0+MX>y>d{Zgt=S+ zk9~cWM4QZ`CE1+Ugz4hts^N8x?#7;Ws-J&za%yjHkAzFNy#}_Q5RY4C{`?0;{7G?> zRg8F!pVsq#kJ#x}Gk9FjQo|{2I{UD+E{10afRm&Mrj#|bYeKzjGr`$tu*1xsvsPAy zxi=qvP`h_T7vxIHPpv~N7c9!34UpcHfxjRxpNwFdORw}oF}SqvFSU%o_u3$xqe|Bo zo9yUdJwvej#XtP6FPz#qC**Z)T@=Y4Esz#(q`@$y8TAg{bUPoyW7;ojFqDQYngvb2 z8xj1iUw2y2=pxiZ|9zFk_x9f%fpDNfg7N3}#B8+7FL@s5#6jkYQlZ&md8;~5rL1k57aF%|ivFYo!1b9EnfJ69JI zUrzfc)DfqmzB(2MAeAAEQdBz+8l2uzRR$7Zo~tmT{2AN*Fw#FA7We#;BnX^a$R_wS zJ zK3w%xtm%-%y__O7w0TIzpb~hqL+0#F-NTa5swN zn{na+pN`OrwNSb|7XYGl={)=@a2PW<3F}%Y(L^NX^3(Mh^NKW$nl^zZogux62`r|c z*9!=je3Ns=1R$?jT;eXFDq~vgdhA-}gtkKVvp#Ej#kz>xOWhs%kunTQfxuIrUW;9*Ol6R$Ic zHCNc+PF%-nuT#nYNV|J*y%YCGhfZ@f>{d`#1+u*p$$J1vz@NLH_>*`D!!}#5FyBlkJJMmXz5uL&CE376@ z>Z7KG)t_|Dlq87wLUp@z!zaqYJVsvm*vVd|o^1#+k|4lSJUL}VK18bxBHIPK1jzQ; z4$-UG)?lxmd1rXqRne9*VQGC3lcCA{^RWjp&CmR9p;RZa9*d*F!2f>#0!ROL zqd|rE)nF=-J|6qtLta=PhX#j;BuOULd>yW5I>B^ z)}$iRJiufY2iO4^5c87A_T4)uaI2(2Vj?o5oUIL*Z#uYBL_mDxMtSTkK0)hR##CoK zDg^Uq7kYKX)0BLtpB0R3I3Y1I1* zP>VmDWN+XbnPWW4F4^4sO=5|lROUk+!{_Z#*TauN&@4d`CVgL%+C-uXQob>q6a;DA zBq8<=ESdydctma?Wvvbp&vq-`Tq{(OVcanel*cbmGYB!gKx$1>^49f?go(khkkKkX zyWr9g#S4(7toaXL%T?ngG9{B5@Et;sodlX4RpnFO=Tcz!;T;!KK`bptwksxkb&hwp z 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�nw~G>7{%O@ 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 diff --git a/public/css/dashboard.styles.css b/public/css/dashboard.styles.css index 631df81..5888c41 100644 --- a/public/css/dashboard.styles.css +++ b/public/css/dashboard.styles.css @@ -638,4 +638,1615 @@ position: relative !important; .notification { max-width: 100%; } +} + +.collaboration-badge { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 4px 8px; + border-radius: 12px; + cursor: pointer; + transition: all 0.3s ease; +} + +.collaboration-badge.badge-info { + background-color: #17a2b8; + color: white; +} + +.collaboration-badge.badge-secondary { + background-color: #6c757d; + color: white; +} + +.toggle-collaboration-btn.active { + background-color: #17a2b8; + border-color: #17a2b8; + color: white; +} + +.toggle-collaboration-btn:not(.active) { + background-color: #6c757d; + border-color: #6c757d; +} + +.collaboration-details { + max-height: 300px; + overflow-y: auto; +} + +.user-item { + display: flex; + align-items: center; + gap: 8px; + padding: 8px; + border-bottom: 1px solid #eee; +} + +.user-avatar { + width: 32px; + height: 32px; + border-radius: 50%; + object-fit: cover; +} + +/* Styles pour le menu contextuel */ +.context-menu { + position: fixed; + z-index: 1000; + background-color: hsl(var(--card)); + border: 1px solid hsl(var(--border)); + border-radius: var(--radius); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + min-width: 180px; + overflow: hidden; /* Pour que les hovers aillent jusqu'au bout */ +} + +/* Style pour les items du menu */ +.menu-item, +.dropdown-item { + display: flex; + align-items: center; + width: 100%; + padding: 0.5rem 1rem; + border: none; + background: none; + color: hsl(var(--foreground)); + font-size: 0.875rem; + text-align: left; + cursor: pointer; + transition: background-color 0.1s ease; + text-decoration: none; /* Pour les liens */ + white-space: nowrap; +} + +/* Hover pour mode clair et sombre */ +.menu-item:hover, +.dropdown-item:hover { + background-color: hsl(var(--accent)); +} + +.dark .menu-item:hover, +.dark .dropdown-item:hover { + background-color: hsl(var(--accent)); + color: hsl(var(--accent-foreground)); +} + +/* Alignement des icônes */ +.menu-item i, +.dropdown-item i { + width: 20px; + margin-right: 0.75rem; + text-align: center; +} + +/* Style pour les items destructifs (suppression) */ +.menu-item.destructive, +.dropdown-item.destructive { + color: hsl(var(--destructive)); +} + +.menu-item.destructive:hover, +.dropdown-item.destructive:hover { + background-color: hsl(var(--destructive)); + color: hsl(var(--destructive-foreground)); +} + +/* Style pour le séparateur */ +.menu-separator, +.dropdown-divider { + height: 1px; + background-color: hsl(var(--border)); + margin: 0; +} + +/* Menu déroulant */ +.dropdown-menu { + background-color: hsl(var(--card)); + border: 1px solid hsl(var(--border)); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + border-radius: var(--radius); + overflow: hidden; + min-width: 180px; +} + +/* Animation */ +@keyframes menuFadeIn { + from { + opacity: 0; + transform: scale(0.95); + } + to { + opacity: 1; + transform: scale(1); + } +} + +.context-menu, +.dropdown-menu.show { + animation: menuFadeIn 0.1s ease-out; +} + +/* Dark mode hover fix */ +.dark .table tr:hover { + background-color: hsl(var(--muted)) !important; +} + +/* Dropdown animation fix */ +.dropdown-menu { + display: none; + opacity: 0; + transform: translateY(-10px); + transition: opacity 0.15s ease-out, transform 0.15s ease-out; +} + +.dropdown-menu.show { + display: block; + opacity: 1; + transform: translateY(0); +} + +/* Double-click styles */ +tr[data-type="folder"], tr[data-type="shared-folder"] { + cursor: pointer; +} + +tr[data-type="folder"]:active, tr[data-type="shared-folder"]:active { + background-color: hsl(var(--accent)); +} + +/* === VUE GRILLE REDESIGNÉE === */ +.grid-view { + display: grid !important; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 1.25rem; + padding: 1.5rem; + width: 100%; + max-width: 100%; + box-sizing: border-box; + animation: gridFadeIn 0.3s ease-out; + justify-content: start; + align-content: start; +} + +.grid-view > tr { + display: block; + position: relative; + background: hsl(var(--card)); + border: 1px solid hsl(var(--border)); + border-radius: 12px; + padding: 0; + text-align: center; + cursor: pointer; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + height: auto; + overflow: hidden; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + max-width: 250px; + min-width: 180px; +} + +.grid-view tr:hover { + transform: translateY(-4px); + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15); + border-color: hsl(var(--primary) / 0.3); +} + +.grid-view td { + display: block; + padding: 0 !important; + border: none !important; +} + +.grid-view td:not(:first-child) { + display: none; +} + +.grid-view .icon-container { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.75rem; + padding: 1.5rem 1rem 1rem; + position: relative; +} + +.grid-view .icon { + width: 56px; + height: 56px; + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, hsl(var(--primary) / 0.1), hsl(var(--primary) / 0.05)); + border-radius: 12px; + margin-bottom: 0.5rem; + transition: transform 0.2s ease; +} + +.grid-view tr:hover .icon { + transform: scale(1.1); +} + +.grid-view .icon i { + font-size: 1.75rem; + color: hsl(var(--primary)); +} + +.grid-view .icon i.fa-folder { + color: #f59e0b; +} + +.grid-view .icon i.fa-file { + color: #3b82f6; +} + +.grid-view .icon i.fa-image { + color: #10b981; +} + +.grid-view .icon i.fa-film { + color: #ef4444; +} + +.grid-view .icon i.fa-file-pdf { + color: #dc2626; +} + +.grid-view .label { + font-size: 0.9rem; + font-weight: 600; + color: hsl(var(--foreground)); + margin: 0; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 2; + line-clamp: 2; + -webkit-box-orient: vertical; + max-width: 100%; + line-height: 1.3; + min-height: 2.6rem; +} + +.grid-view .details { + font-size: 0.75rem; + color: hsl(var(--muted-foreground)); + margin-top: 0.25rem; + padding: 0.5rem 0; + border-top: 1px solid hsl(var(--border) / 0.5); + width: 100%; +} + +/* Actions pour la vue grille */ +.grid-view .grid-dropdown { + position: absolute; + top: 0.5rem; + right: 0.5rem; + opacity: 0; + transition: all 0.2s ease; + z-index: 10; +} + +.grid-view tr:hover .grid-dropdown { + opacity: 1; +} + +.grid-view .grid-dropdown .dropdown-toggle { + background: rgba(255, 255, 255, 0.9); + border: 1px solid hsl(var(--border)); + border-radius: 6px; + padding: 0.25rem 0.5rem; + font-size: 0.75rem; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.grid-view .grid-dropdown .dropdown-menu { + font-size: 0.85rem; + min-width: 140px; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); +} + +/* Badge de collaboration pour la vue grille */ +.grid-view .collaboration-badge { + position: absolute; + top: 0.75rem; + left: 0.75rem; + font-size: 0.65rem; + padding: 0.25rem 0.5rem; + border-radius: 8px; + background: hsl(var(--primary)); + color: hsl(var(--primary-foreground)); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +/* Styles améliorés pour la vue liste */ +.table:not(.grid-view) tr { + transition: background-color 0.2s ease; +} + +.table:not(.grid-view) td { + vertical-align: middle; + padding: 1rem 0.75rem; +} + +.table:not(.grid-view) td:first-child { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.table:not(.grid-view) .icon i { + font-size: 1.2rem; + width: 24px; + text-align: center; +} + +/* Bouton de basculement vue grille/liste */ +.view-toggle-btn { + background: hsl(var(--secondary)); + color: hsl(var(--secondary-foreground)); + border: 1px solid hsl(var(--border)); + padding: 0.5rem; + border-radius: var(--radius); + cursor: pointer; + transition: all 0.2s ease; +} + +.view-toggle-btn:hover { + background: hsl(var(--accent)); + color: hsl(var(--accent-foreground)); +} + +.view-toggle-btn i { + font-size: 1.1rem; +} + +/* Animation de transition entre les vues */ +.table tbody { + transition: all 0.3s ease; +} + +/* Animations pour la vue grille */ +@keyframes gridFadeIn { + from { + opacity: 0; + transform: scale(0.95); + } + to { + opacity: 1; + transform: scale(1); + } +} + +@keyframes itemSlideIn { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.grid-view tr { + animation: itemSlideIn 0.3s ease-out backwards; +} + +.grid-view tr:nth-child(1) { animation-delay: 0.05s; } +.grid-view tr:nth-child(2) { animation-delay: 0.1s; } +.grid-view tr:nth-child(3) { animation-delay: 0.15s; } +.grid-view tr:nth-child(4) { animation-delay: 0.2s; } +.grid-view tr:nth-child(5) { animation-delay: 0.25s; } +.grid-view tr:nth-child(6) { animation-delay: 0.3s; } + +/* Responsive adjustments */ +@media (max-width: 768px) { + .grid-view { + grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); + gap: 1rem; + padding: 1rem; + justify-content: center; + } + + .grid-view .icon { + width: 48px; + height: 48px; + } + + .grid-view .icon i { + font-size: 1.5rem; + } + + .grid-view .label { + font-size: 0.85rem; + } + + .grid-view .details { + font-size: 0.7rem; + } +} + +@media (max-width: 480px) { + .grid-view { + grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); + gap: 0.75rem; + padding: 0.75rem; + justify-content: center; + } + + .grid-view .icon-container { + padding: 1rem 0.5rem 0.75rem; + } +} + +/* Conteneur du tableau pour éviter l'espace vide */ +#fileTable { + width: 100%; + table-layout: auto; +} + +.grid-view.table { + table-layout: unset; + width: auto; + max-width: 100%; +} + +/* Dark mode adjustments */ +.dark .grid-view tr { + background: hsl(var(--card)); + border-color: hsl(var(--border)); +} + +.dark .grid-view tr:hover { + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3); +} + +.dark .grid-view .grid-dropdown .dropdown-toggle { + background: rgba(0, 0, 0, 0.8); + color: hsl(var(--foreground)); +} + +/* Correction pour éviter l'espace vide à droite */ +.table.grid-view { + display: grid !important; + table-layout: unset; + border-collapse: unset; +} + +.table-responsive .grid-view { + overflow: visible; +} + +.grid-view tbody { + display: contents; +} + +.grid-view thead, +.grid-view tbody tr:empty { + display: none; +} + +@keyframes fadeScale { + from { + opacity: 0; + transform: scale(0.98); + } + to { + opacity: 1; + transform: scale(1); + } +} + +.grid-view { + animation: fadeScale 0.2s ease-out; +} + +/* Modal Collaboration - Design moderne */ +.collaboration-modal { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(8px); + display: flex; + align-items: center; + justify-content: center; + z-index: 1050; + opacity: 0; + visibility: hidden; + transition: all 0.3s ease; +} + +.collaboration-modal.show { + opacity: 1; + visibility: visible; +} + +.collaboration-modal .modal-content { + background: hsl(var(--card)); + border: 1px solid hsl(var(--border)); + border-radius: 16px; + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15); + max-width: 600px; + width: 90%; + max-height: 90vh; + overflow: hidden; + transform: scale(0.9) translateY(20px); + transition: transform 0.3s ease; +} + +.collaboration-modal.show .modal-content { + transform: scale(1) translateY(0); +} + +.collaboration-modal .modal-header { + padding: 2rem 2rem 1rem 2rem; + border-bottom: 1px solid hsl(var(--border)); + display: flex; + align-items: center; + justify-content: space-between; +} + +.collaboration-modal .modal-title { + font-size: 1.5rem; + font-weight: 600; + color: hsl(var(--foreground)); + margin: 0; + display: flex; + align-items: center; + gap: 0.75rem; +} + +.collaboration-modal .modal-title i { + color: hsl(var(--primary)); +} + +.collaboration-modal .close { + background: none; + border: none; + font-size: 1.5rem; + cursor: pointer; + color: hsl(var(--muted-foreground)); + padding: 0.5rem; + border-radius: 8px; + transition: all 0.2s ease; +} + +.collaboration-modal .close:hover { + background: hsl(var(--accent)); + color: hsl(var(--accent-foreground)); +} + +.collaboration-modal .modal-body { + padding: 1.5rem 2rem; + max-height: 60vh; + overflow-y: auto; +} + +.collaboration-modal .modal-footer { + padding: 1rem 2rem 2rem 2rem; + border-top: 1px solid hsl(var(--border)); + display: flex; + justify-content: space-between; + align-items: center; + gap: 1rem; +} + +/* Section des utilisateurs collaborateurs */ +.collaboration-users { + margin-bottom: 2rem; +} + +.collaboration-users h6 { + font-size: 1rem; + font-weight: 600; + color: hsl(var(--foreground)); + margin-bottom: 1rem; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.collaboration-user-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1rem; + background: hsl(var(--muted) / 0.3); + border: 1px solid hsl(var(--border)); + border-radius: 12px; + margin-bottom: 0.75rem; + transition: all 0.2s ease; +} + +.collaboration-user-item:hover { + background: hsl(var(--muted) / 0.5); + transform: translateY(-1px); +} + +.collaboration-user-info { + display: flex; + align-items: center; + gap: 1rem; +} + +.collaboration-user-avatar { + width: 40px; + height: 40px; + border-radius: 50%; + object-fit: cover; + border: 2px solid hsl(var(--border)); +} + +.collaboration-user-details h6 { + margin: 0; + font-size: 0.95rem; + font-weight: 500; + color: hsl(var(--foreground)); +} + +.collaboration-user-details small { + color: hsl(var(--muted-foreground)); + font-size: 0.8rem; +} + +.collaboration-user-status { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.status-indicator { + width: 8px; + height: 8px; + border-radius: 50%; + background: hsl(var(--primary)); + animation: pulse 2s infinite; +} + +.status-indicator.offline { + background: hsl(var(--muted-foreground)); + animation: none; +} + +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} + +/* Section d'ajout de collaborateur */ +.add-collaborator { + padding: 1.5rem; + background: hsl(var(--accent) / 0.1); + border: 1px dashed hsl(var(--border)); + border-radius: 12px; +} + +.add-collaborator h6 { + font-size: 1rem; + font-weight: 600; + color: hsl(var(--foreground)); + margin-bottom: 1rem; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.search-input-group { + position: relative; + margin-bottom: 1rem; +} + +.search-input-group input { + width: 100%; + padding: 0.75rem 1rem 0.75rem 2.5rem; + border: 1px solid hsl(var(--border)); + border-radius: 8px; + background: hsl(var(--background)); + color: hsl(var(--foreground)); + font-size: 0.9rem; + transition: all 0.2s ease; +} + +.search-input-group input:focus { + outline: none; + border-color: hsl(var(--primary)); + box-shadow: 0 0 0 3px hsl(var(--primary) / 0.1); +} + +.search-input-group i { + position: absolute; + left: 0.75rem; + top: 50%; + transform: translateY(-50%); + color: hsl(var(--muted-foreground)); + font-size: 0.9rem; +} + +.search-results { + max-height: 200px; + overflow-y: auto; + border: 1px solid hsl(var(--border)); + border-radius: 8px; + background: hsl(var(--background)); + margin-top: 0.5rem; +} + +.user-result { + padding: 0.75rem 1rem; + border-bottom: 1px solid hsl(var(--border)); + transition: background 0.2s ease; +} + +.user-result:last-child { + border-bottom: none; +} + +.user-result:hover { + background: hsl(var(--accent) / 0.2); +} + +.user-result .user-info { + display: flex; + align-items: center; + justify-content: space-between; +} + +.user-result .user-avatar { + width: 32px; + height: 32px; + border-radius: 50%; + margin-right: 0.75rem; + object-fit: cover; +} + +.user-result .user-name { + font-weight: 500; + color: hsl(var(--foreground)); +} + +.user-result .btn { + padding: 0.4rem 0.8rem; + font-size: 0.8rem; + border-radius: 6px; +} + +/* État vide */ +.empty-state { + text-align: center; + padding: 2rem 1rem; + color: hsl(var(--muted-foreground)); +} + +.empty-state i { + font-size: 2.5rem; + margin-bottom: 1rem; + opacity: 0.5; +} + +.empty-state p { + margin: 0; + font-size: 0.9rem; +} + +/* Context Menu amélioré */ +.context-menu { + min-width: 220px; + padding: 0.5rem; +} + +.context-menu .menu-item { + border-radius: var(--radius); + margin: 0.2rem 0; +} + +/* Style du menu contextuel en mode sombre */ +.dark .context-menu { + background-color: hsl(var(--card)); + border-color: hsl(var(--border)); +} + +.dark .context-menu .menu-item { + color: hsl(var(--foreground)); +} + +.dark .context-menu .menu-item:hover { + background-color: hsl(var(--accent)); + color: hsl(var(--accent-foreground)); +} + +/* Correction des modales en mode sombre */ +.dark .modal-content { + background-color: hsl(var(--card)); + color: hsl(var(--foreground)); +} + +.dark .modal-header { + border-bottom-color: hsl(var(--border)); +} + +.dark .modal-footer { + border-top-color: hsl(var(--border)); +} + +.dark .modal .close { + color: hsl(var(--foreground)); +} + +.dark .modal-body { + color: hsl(var(--foreground)); +} + +.dark .form-control { + background-color: hsl(var(--background)); + border-color: hsl(var(--border)); + color: hsl(var(--foreground)); +} + +/* SweetAlert2 Dark Mode */ +.dark .swal2-popup { + background-color: hsl(var(--card)) !important; + color: hsl(var(--foreground)) !important; +} + +.dark .swal2-title { + color: hsl(var(--foreground)) !important; +} + +.dark .swal2-content { + color: hsl(var(--foreground)) !important; +} + +.dark .swal2-input { + background-color: hsl(var(--background)) !important; + border-color: hsl(var(--border)) !important; + color: hsl(var(--foreground)) !important; +} + +/* Nouveau style pour le bouton de copie de lien */ +.copy-link-btn { + display: flex; + align-items: center; + gap: 0.5rem; + width: 100%; + padding: 0.5rem 1rem; + color: hsl(var(--foreground)); + background: none; + border: none; + cursor: pointer; + transition: all 0.2s ease; +} + +.copy-link-btn:hover { + background-color: hsl(var(--accent)); + color: hsl(var(--accent-foreground)); +} + +.copy-link-btn i { + width: 20px; + text-align: center; +} + +/* =================== MODAL COLLABORATION MODERNE =================== */ +.collaboration-modal { + display: none !important; + position: fixed !important; + top: 0 !important; + left: 0 !important; + width: 100% !important; + height: 100% !important; + z-index: 1050 !important; + background-color: rgba(0, 0, 0, 0.6) !important; + backdrop-filter: blur(12px) !important; + -webkit-backdrop-filter: blur(12px) !important; + animation: overlayShow 0.15s cubic-bezier(0.16, 1, 0.3, 1) !important; + justify-content: center !important; + align-items: center !important; +} + +.collaboration-modal.show, +.collaboration-modal[style*="display: block"] { + display: flex !important; +} + +.collaboration-modal .modal-dialog { + position: relative !important; + width: 90vw !important; + max-width: 580px !important; + max-height: 85vh !important; + margin: 0 !important; + transform: none !important; +} + +.collaboration-modal .modal-content { + background: hsl(var(--card)) !important; + border-radius: 16px !important; + border: 1px solid hsl(var(--border)) !important; + box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25), 0 0 0 1px rgba(255, 255, 255, 0.05) !important; + animation: contentShow 0.15s cubic-bezier(0.16, 1, 0.3, 1) !important; + overflow: hidden !important; + position: relative !important; + width: 100% !important; +} + +.collaboration-modal .modal-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 20px 24px 16px; + border-bottom: 1px solid hsl(var(--border)); + background: hsl(var(--card)); +} + +.collaboration-modal .modal-title-wrapper { + display: flex; + align-items: center; + gap: 12px; +} + +.collaboration-modal .modal-icon { + width: 24px; + height: 24px; + color: hsl(var(--primary)); + font-size: 20px; +} + +.collaboration-modal .modal-title { + margin: 0; + font-size: 18px; + font-weight: 600; + color: hsl(var(--foreground)); +} + +.collaboration-modal .close { + background: none; + border: none; + color: hsl(var(--muted-foreground)); + width: 32px; + height: 32px; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.15s ease; + cursor: pointer; + font-size: 16px; + padding: 0; +} + +.collaboration-modal .close:hover { + background: hsl(var(--accent)); + color: hsl(var(--accent-foreground)); +} + +.collaboration-modal .modal-body { + padding: 24px; + max-height: calc(85vh - 140px); + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 24px; +} + +/* Sections */ +.collaboration-section { + background: hsl(var(--background)); + border-radius: 12px; + border: 1px solid hsl(var(--border)); + overflow: hidden; +} + +.collaboration-section-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px 20px; + background: hsl(var(--muted) / 0.3); + border-bottom: 1px solid hsl(var(--border)); +} + +.section-title-group { + display: flex; + align-items: center; + gap: 10px; +} + +.section-icon { + width: 18px; + height: 18px; + color: hsl(var(--primary)); + font-size: 16px; +} + +.collaboration-section-title { + margin: 0; + font-size: 16px; + font-weight: 600; + color: hsl(var(--foreground)); +} + +.collaboration-count-badge { + background: hsl(var(--primary)); + color: hsl(var(--primary-foreground)); + padding: 4px 8px; + border-radius: 12px; + font-size: 12px; + font-weight: 600; + min-width: 24px; + text-align: center; +} + +/* Liste des utilisateurs */ +.collaboration-users-list { + padding: 16px 20px; +} + +.collaboration-user-item { + display: flex; + align-items: center; + gap: 16px; + padding: 16px; + background: hsl(var(--card)); + border: 1px solid hsl(var(--border)); + border-radius: 12px; + margin-bottom: 12px; + transition: all 0.2s ease; +} + +.collaboration-user-item:last-child { + margin-bottom: 0; +} + +.collaboration-user-item:hover { + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); + border-color: hsl(var(--primary) / 0.5); +} + +.user-avatar-wrapper { + position: relative; + flex-shrink: 0; +} + +.user-avatar { + width: 48px; + height: 48px; + border-radius: 50%; + object-fit: cover; + border: 2px solid hsl(var(--border)); +} + +.user-status-indicator { + position: absolute; + bottom: 2px; + right: 2px; + width: 14px; + height: 14px; + background: #10b981; + border: 2px solid hsl(var(--card)); + border-radius: 50%; + animation: pulse 2s infinite; +} + +.user-details { + flex: 1; + min-width: 0; +} + +.user-name { + font-size: 16px; + font-weight: 600; + color: hsl(var(--foreground)); + margin-bottom: 4px; +} + +.user-role { + display: flex; + align-items: center; + gap: 6px; + font-size: 13px; + color: hsl(var(--muted-foreground)); +} + +.user-role i { + font-size: 12px; + color: hsl(var(--primary)); +} + +.user-actions { + display: flex; + gap: 8px; +} + +.action-btn { + width: 36px; + height: 36px; + border: none; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.15s ease; + font-size: 14px; +} + +.remove-btn { + background: rgba(239, 68, 68, 0.1); + color: #ef4444; +} + +.remove-btn:hover { + background: #ef4444; + color: white; + transform: scale(1.05); +} + +/* État vide */ +.collaboration-empty-state { + text-align: center; + padding: 40px 20px; + color: hsl(var(--muted-foreground)); +} + +.empty-icon { + font-size: 48px; + color: hsl(var(--muted-foreground)); + margin-bottom: 16px; + opacity: 0.5; +} + +.collaboration-empty-state p { + font-size: 16px; + font-weight: 500; + margin: 0 0 8px 0; + color: hsl(var(--foreground)); +} + +.collaboration-empty-state span { + font-size: 14px; + color: hsl(var(--muted-foreground)); +} + +/* Conteneur de recherche */ +.collaboration-search-container { + padding: 20px; +} + +.search-input-group { + display: flex; + gap: 12px; + margin-bottom: 16px; +} + +.search-input-wrapper { + position: relative; + flex: 1; +} + +.search-icon { + position: absolute; + left: 12px; + top: 50%; + transform: translateY(-50%); + color: hsl(var(--muted-foreground)); + font-size: 14px; +} + +.search-input { + width: 100%; + padding: 12px 12px 12px 40px; + border: 1px solid hsl(var(--border)); + border-radius: 8px; + background: hsl(var(--card)); + color: hsl(var(--foreground)); + font-size: 14px; + outline: none; + transition: all 0.15s ease; +} + +.search-input:focus { + border-color: hsl(var(--primary)); + box-shadow: 0 0 0 3px hsl(var(--primary) / 0.1); +} + +.search-input::placeholder { + color: hsl(var(--muted-foreground)); +} + +.search-btn { + padding: 12px 20px; + border: none; + border-radius: 8px; + background: hsl(var(--primary)); + color: hsl(var(--primary-foreground)); + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.15s ease; + display: flex; + align-items: center; + gap: 8px; + white-space: nowrap; +} + +.search-btn:hover { + background: hsl(var(--primary) / 0.9); + transform: translateY(-1px); +} + +/* Résultats de recherche */ +.search-results { + min-height: 60px; +} + +.search-loading, +.search-no-result, +.search-error { + display: flex; + align-items: center; + justify-content: center; + gap: 12px; + padding: 24px; + background: hsl(var(--muted) / 0.3); + border-radius: 8px; + color: hsl(var(--muted-foreground)); + font-size: 14px; +} + +.search-loading i { + color: hsl(var(--primary)); +} + +.search-error { + color: #ef4444; +} + +.search-result-item { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 16px; + background: hsl(var(--card)); + border: 1px solid hsl(var(--border)); + border-radius: 8px; + animation: slideInUp 0.3s ease; +} + +.result-user-info { + display: flex; + align-items: center; + gap: 12px; + flex: 1; +} + +.result-avatar-wrapper { + flex-shrink: 0; +} + +.result-avatar { + width: 40px; + height: 40px; + border-radius: 50%; + object-fit: cover; + border: 2px solid hsl(var(--border)); +} + +.result-details { + flex: 1; +} + +.result-name { + font-size: 14px; + font-weight: 600; + color: hsl(var(--foreground)); + margin-bottom: 2px; +} + +.result-status { + display: flex; + align-items: center; + gap: 6px; + font-size: 12px; + color: hsl(var(--muted-foreground)); +} + +.result-status i { + color: #10b981; + font-size: 11px; +} + +.add-user-btn { + padding: 8px 16px; + border: none; + border-radius: 6px; + background: hsl(var(--primary)); + color: hsl(var(--primary-foreground)); + font-size: 13px; + font-weight: 500; + cursor: pointer; + transition: all 0.15s ease; + display: flex; + align-items: center; + gap: 6px; + white-space: nowrap; +} + +.add-user-btn:hover { + background: hsl(var(--primary) / 0.9); + transform: translateY(-1px); +} + +/* Footer et boutons */ +.collaboration-modal .modal-footer { + border-top: 1px solid hsl(var(--border)); + padding: 16px 24px; + display: flex; + justify-content: flex-end; + gap: 12px; + background: hsl(var(--background) / 0.5); +} + +.collaboration-modal .btn { + border-radius: 8px; + padding: 10px 16px; + font-weight: 500; + font-size: 14px; + transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1); + border: none; + cursor: pointer; + display: flex; + align-items: center; + gap: 8px; +} + +.collaboration-modal .btn:hover { + transform: translateY(-1px); +} + +.collaboration-modal .btn-danger { + background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); + color: white; +} + +.collaboration-modal .btn-danger:hover { + background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%); + box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3); +} + +.collaboration-modal .btn-secondary { + background: hsl(var(--secondary)); + color: hsl(var(--secondary-foreground)); + border: 1px solid hsl(var(--border)); +} + +.collaboration-modal .btn-secondary:hover { + background: hsl(var(--accent)); + color: hsl(var(--accent-foreground)); +} + +/* Animations */ +@keyframes overlayShow { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes contentShow { + from { + opacity: 0; + transform: scale(0.96); + } + to { + opacity: 1; + transform: scale(1); + } +} + +@keyframes slideInUp { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes pulse { + 0%, 100% { + opacity: 1; + transform: scale(1); + } + 50% { + opacity: 0.6; + transform: scale(0.95); + } +} + +/* Badge de collaboration amélioré */ +.collaboration-badge { + background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%); + color: white; + padding: 0.25rem 0.75rem; + border-radius: 20px; + font-size: 0.75rem; + font-weight: 500; + display: inline-flex; + align-items: center; + gap: 0.25rem; + transition: all 0.2s ease; + animation: fadeIn 0.3s ease; +} + +.collaboration-badge.badge-secondary { + background: linear-gradient(135deg, #6b7280 0%, #4b5563 100%); +} + +.collaboration-badge:hover { + transform: scale(1.05); +} + +.active-users-count { + background: rgba(255, 255, 255, 0.2); + border-radius: 10px; + padding: 0.125rem 0.375rem; + font-size: 0.6875rem; + font-weight: 600; +} + +/* Mode sombre */ +.dark .collaboration-modal .modal-content { + background: hsl(var(--card)); + border-color: hsl(var(--border)); + box-shadow: + 0 25px 50px -12px rgba(0, 0, 0, 0.5), + 0 0 0 1px rgba(255, 255, 255, 0.1); +} + +.dark .collaboration-section { + background: hsl(var(--background)); + border-color: hsl(var(--border)); +} + +.dark .collaboration-user-item { + background: hsl(var(--background)); + border-color: hsl(var(--border)); +} + +.dark .search-input { + background: hsl(var(--background)); + color: hsl(var(--foreground)); + border-color: hsl(var(--border)); +} + +.dark .search-result-item { + background: hsl(var(--background)); + border-color: hsl(var(--border)); +} + +/* Responsive */ +@media (max-width: 640px) { + .collaboration-modal .modal-dialog { + width: 95vw; + max-width: none; + margin: 8px; + } + + .collaboration-modal .modal-header, + .collaboration-modal .modal-body { + padding-left: 16px; + padding-right: 16px; + } + + .collaboration-modal .modal-footer { + padding: 12px 16px; + flex-direction: column; + gap: 8px; + } + + .collaboration-modal .btn { + width: 100%; + justify-content: center; + } + + .search-input-group { + flex-direction: column; + } + + .search-btn { + width: 100%; + justify-content: center; + } + + .collaboration-user-item { + padding: 12px; + gap: 12px; + } + + .user-avatar { + width: 40px; + height: 40px; + } + + .search-result-item { + flex-direction: column; + align-items: stretch; + gap: 12px; + } + + .result-user-info { + justify-content: center; + } + + .add-user-btn { + width: 100%; + justify-content: center; + } + + .user-item { + padding: 10px; + } + + .user-avatar { + width: 36px; + height: 36px; + } +} + +/* Badge de collaboration amélioré */ +.collaboration-badge { + background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%); + color: white; + padding: 0.25rem 0.75rem; + border-radius: 20px; + font-size: 0.75rem; + font-weight: 500; + display: inline-flex; + align-items: center; + gap: 0.25rem; + transition: all 0.2s ease; + animation: fadeIn 0.3s ease; +} + +.collaboration-badge.badge-secondary { + background: linear-gradient(135deg, #6b7280 0%, #4b5563 100%); +} + +.collaboration-badge:hover { + transform: scale(1.05); +} + +.active-users-count { + background: rgba(255, 255, 255, 0.2); + border-radius: 10px; + padding: 0.125rem 0.375rem; + font-size: 0.6875rem; + font-weight: 600; } \ No newline at end of file diff --git a/public/js/dashboard-old.js b/public/js/dashboard-old.js new file mode 100644 index 0000000..b81ec16 --- /dev/null +++ b/public/js/dashboard-old.js @@ -0,0 +1,2169 @@ +document.addEventListener('DOMContentLoaded', function() { + document.querySelectorAll('.file-size').forEach(function(element) { + const size = parseInt(element.getAttribute('data-size')); + element.textContent = formatFileSize(size); + }); + + document.getElementById('searchButton').addEventListener('click', searchFiles); + document.getElementById('newFolderBtn').addEventListener('click', showNewFolderModal); + + document.querySelectorAll('.delete-folder-button').forEach(button => { + button.addEventListener('click', function() { + const folderName = this.getAttribute('data-folder-name'); + confirmDeleteFolder(folderName); + }); + }); + + document.querySelectorAll('.delete-file-button').forEach(button => { + button.addEventListener('click', function() { + const fileName = this.getAttribute('data-file-name'); + confirmDelete(fileName); + }); + }); + + document.querySelectorAll('.copy-button').forEach(button => { + button.addEventListener('click', function() { + const fileUrl = this.getAttribute('data-file-url'); + copyFileLink(fileUrl); + }); + }); + + document.querySelectorAll('.rename-file-btn').forEach(button => { + button.addEventListener('click', function() { + const fileName = this.getAttribute('data-file-name'); + const folderName = this.getAttribute('data-folder-name'); + renameFile(folderName, fileName); + }); + }); + + document.querySelectorAll('.move-file-btn').forEach(button => { + button.addEventListener('click', function() { + const fileName = this.getAttribute('data-file-name'); + showMoveFileModal(fileName); + }); + }); + + document.getElementById('confirmMoveFile').addEventListener('click', moveFile); + document.getElementById('themeSwitcher').addEventListener('click', toggleDarkMode); + + initTheme(); + + document.addEventListener('DOMContentLoaded', function () { + const accountDropdownBtn = document.getElementById('accountDropdownBtn'); + const accountDropdownMenu = document.getElementById('accountDropdownMenu'); + + accountDropdownBtn.addEventListener('click', function (e) { + e.stopPropagation(); + accountDropdownMenu.classList.toggle('show'); + }); + + document.addEventListener('click', function (e) { + if (!accountDropdownBtn.contains(e.target) && !accountDropdownMenu.contains(e.target)) { + accountDropdownMenu.classList.remove('show'); + } + }); + }); + + $('.modal').modal({ + show: false + }); + + const metadataLink = document.querySelector('a[onclick="displayMetadata()"]'); + if (metadataLink) { + metadataLink.addEventListener('click', function(event) { + event.preventDefault(); + displayMetadata(); + }); + } + + document.querySelectorAll('[onclick^="showFileInfo"]').forEach(link => { + link.addEventListener('click', function(event) { + event.preventDefault(); + const fileName = this.getAttribute('onclick').match(/'([^']+)'/)[1]; + showFileInfo(fileName); + }); + }); + }); + + function formatFileSize(fileSizeInBytes) { + if (fileSizeInBytes < 1024) return fileSizeInBytes + ' octets'; + else if (fileSizeInBytes < 1048576) return (fileSizeInBytes / 1024).toFixed(2) + ' Ko'; + else if (fileSizeInBytes < 1073741824) return (fileSizeInBytes / 1048576).toFixed(2) + ' Mo'; + else return (fileSizeInBytes / 1073741824).toFixed(2) + ' Go'; + } + + function searchFiles() { + const input = document.getElementById('searchInput'); + const filter = input.value.toUpperCase(); + const table = document.getElementById('fileTable'); + const tr = table.getElementsByTagName('tr'); + + for (let i = 1; i < tr.length; i++) { + const td = tr[i].getElementsByTagName('td')[0]; + if (td) { + const txtValue = td.textContent || td.innerText; + if (txtValue.toUpperCase().indexOf(filter) > -1) { + tr[i].style.display = ""; + } else { + tr[i].style.display = "none"; + } + } + } + } + + function showNewFolderModal() { + Swal.fire({ + title: 'Nouveau dossier', + input: 'text', + inputPlaceholder: 'Entrer le nom du nouveau dossier', + confirmButtonText: 'Créer', + showCancelButton: true, + cancelButtonText: 'Annuler', + preConfirm: (folderName) => { + if (!folderName) { + Swal.showValidationMessage('Le nom du dossier ne peut pas être vide.'); + } + return folderName; + } + }).then(result => { + if (result.isConfirmed) { + createNewFolder(result.value); + } + }); + } + + function createNewFolder(folderName) { + fetch('/api/dpanel/dashboard/newfolder', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ folderName }), + }) + .then(response => { + if (response.ok) { + return response.json(); + } else { + return response.json().then(error => Promise.reject(error)); + } + }) + .then(result => { + Swal.fire({ + position: 'top', + icon: 'success', + title: 'Le dossier a été créé avec succès.', + showConfirmButton: false, + timer: 2000, + toast: true + }).then(() => { + location.reload(); + }); + }) + .catch(error => { + Swal.fire({ + position: 'top', + icon: 'error', + title: 'Erreur lors de la création du dossier.', + text: error.message, + showConfirmButton: false, + timer: 2350, + toast: true + }); + }); + } + + function confirmDeleteFolder(folderName) { + Swal.fire({ + title: 'Êtes-vous sûr?', + text: `La suppression du dossier "${folderName}" est irréversible!`, + icon: 'warning', + showCancelButton: true, + confirmButtonColor: '#d33', + cancelButtonColor: '#3085d6', + confirmButtonText: 'Supprimer', + cancelButtonText: 'Annuler', + }).then((result) => { + if (result.isConfirmed) { + deleteFolder(folderName); + } + }); + } + + function deleteFolder(folderName) { + fetch(`/api/dpanel/dashboard/deletefolder/${folderName}`, { + method: 'DELETE', + }) + .then(response => { + if (response.ok) { + Swal.fire({ + position: 'top', + icon: 'success', + title: 'Le dossier a été supprimé avec succès.', + showConfirmButton: false, + timer: 1800, + toast: true + }).then(() => { + location.reload(); + }); + } else { + throw new Error('La suppression du dossier a échoué'); + } + }) + .catch(error => { + Swal.fire({ + position: 'top', + icon: 'error', + title: error.message, + showConfirmButton: false, + timer: 1800, + toast: true + }); + }); + } + + function confirmDelete(filename) { + Swal.fire({ + title: 'Êtes-vous sûr de vouloir supprimer ce fichier?', + text: 'Cette action est irréversible!', + icon: 'warning', + showCancelButton: true, + confirmButtonColor: '#d33', + cancelButtonColor: '#3085d6', + confirmButtonText: 'Supprimer' + }).then((result) => { + if (result.isConfirmed) { + deleteFile(filename); + } + }); + } + + function deleteFile(filename) { + fetch('/api/dpanel/dashboard/delete', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + filename: filename, + }), + }) + .then(response => { + if (response.ok) { + Swal.fire({ + position: 'top', + icon: 'success', + title: 'Le fichier a été supprimé avec succès.', + showConfirmButton: false, + timer: 1800, + toast: true + }).then(() => { + location.reload(); + }); + } else { + throw new Error('La suppression du fichier a échoué'); + } + }) + .catch(error => { + Swal.fire({ + position: 'top', + icon: 'error', + title: error.message, + showConfirmButton: false, + timer: 1800, + toast: true + }); + }); + } + + function copyFileLink(fileUrl) { + navigator.clipboard.writeText(fileUrl).then(() => { + Swal.fire({ + position: 'top', + icon: 'success', + title: 'Lien copié !', + showConfirmButton: false, + timer: 1500, + toast: true + }); + }, (err) => { + console.error('Erreur lors de la copie: ', err); + }); + } + + function renameFile(folderName, currentName) { + Swal.fire({ + title: 'Entrez le nouveau nom', + input: 'text', + inputValue: currentName, + inputPlaceholder: 'Nouveau nom', + showCancelButton: true, + confirmButtonText: 'Renommer', + cancelButtonText: 'Annuler', + inputValidator: (value) => { + if (!value) { + return 'Vous devez entrer un nom de fichier'; + } + } + }).then((result) => { + if (result.isConfirmed) { + const newName = result.value; + fetch(`/api/dpanel/dashboard/rename/${folderName}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ currentName: currentName, newName: newName }), + }) + .then(response => { + if (!response.ok) { + throw new Error('Network response was not ok'); + } + return response.json(); + }) + .then(data => { + Swal.fire({ + position: 'top', + icon: 'success', + title: 'Le fichier a été renommé avec succès.', + showConfirmButton: false, + timer: 1800, + toast: true, + }).then(() => { + location.reload(); + }); + }) + .catch((error) => { + Swal.fire({ + position: 'top', + icon: 'error', + title: 'Erreur lors du renommage du fichier.', + text: error.message, + showConfirmButton: false, + timer: 1800, + toast: true, + }); + }); + } + }); + } + + function showMoveFileModal(fileName) { + // Récupérer les dossiers depuis le tableau + const folders = Array.from(document.querySelectorAll('tr[data-type="folder"]')) + .map(folderRow => ({ + name: folderRow.dataset.name, + value: folderRow.dataset.name // On s'assure d'avoir une valeur correcte + })); + + Swal.fire({ + title: 'Déplacer le fichier', + html: ` + + `, + showCancelButton: true, + confirmButtonText: 'Déplacer', + cancelButtonText: 'Annuler', + preConfirm: () => { + const select = document.getElementById('moveFolderSelect'); + const folderName = select.value; + if (!folderName) { + Swal.showValidationMessage('Veuillez sélectionner un dossier'); + return false; + } + return folderName; // On retourne directement la valeur du dossier + } + }).then((result) => { + if (result.isConfirmed && result.value) { + moveFile(fileName, result.value); + } + }); + } + + function moveFile(fileName, folderName) { + // Log pour debug + console.log('Moving file:', { fileName, folderName }); + + fetch('/api/dpanel/dashboard/movefile', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + fileName: fileName, + folderName: folderName // Maintenant c'est une chaîne de caractères valide + }), + }) + .then(response => { + if (!response.ok) { + throw new Error('Network response was not ok'); + } + return response.json(); + }) + .then(data => { + if (data.message === "File moved successfully") { + Swal.fire({ + position: 'top', + icon: 'success', + title: 'Le fichier a été déplacé avec succès.', + showConfirmButton: false, + timer: 1800, + toast: true, + }).then(() => { + location.reload(); + }); + } else { + throw new Error(data.error || 'Une erreur est survenue'); + } + }) + .catch((error) => { + Swal.fire({ + position: 'top', + icon: 'error', + title: 'Erreur lors du déplacement du fichier.', + text: error.message, + showConfirmButton: false, + timer: 1800, + toast: true, + }); + }); + } + + const body = document.body; + const themeSwitcher = document.getElementById('themeSwitcher'); + + function setTheme(theme) { + if (theme === 'dark') { + body.classList.add('dark'); + } else { + body.classList.remove('dark'); + } + localStorage.setItem('theme', theme); + } + + const savedTheme = localStorage.getItem('theme'); + if (savedTheme) { + setTheme(savedTheme); + } else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { + setTheme('dark'); + } + + themeSwitcher.addEventListener('click', function() { + if (body.classList.contains('dark')) { + setTheme('light'); + } else { + setTheme('dark'); + } + }); + + async function showFileInfo(fileName) { + try { + const response = await fetch('/api/dpanel/dashboard/getmetadatafile/file_info', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + fileLink: fileName, + }) + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + const fileInfo = data.find(file => file.fileName === fileName); + + if (!fileInfo) { + throw new Error(`No information found for the file ${fileName}.`); + } + + let html = `

Nom du fichier : ${fileInfo.fileName}

`; + if (fileInfo.expiryDate) { + html += `

Date d'expiration : ${fileInfo.expiryDate}

`; + } + if (fileInfo.password) { + html += `

Mot de passe : Oui

`; + } + if (fileInfo.userId) { + html += `

Utilisateur : ${fileInfo.userId}

`; + } + + Swal.fire({ + title: 'Informations sur le fichier', + html: html, + confirmButtonText: 'Fermer' + }); + } catch (error) { + console.error('Error in showFileInfo:', error); + Swal.fire({ + position: 'top', + icon: 'error', + title: 'Les informations sur le fichier ne sont pas disponibles pour le moment.', + text: `Erreur : ${error.message}`, + showConfirmButton: false, + timer: 1800, + toast: true, + }); + } + } + + function displayMetadata() { + fetch('/build-metadata') + .then(response => response.json()) + .then(metadata => { + document.getElementById('buildVersion').textContent = metadata.build_version; + document.getElementById('nodeVersion').textContent = metadata.node_version; + document.getElementById('expressVersion').textContent = metadata.express_version; + document.getElementById('buildSha').textContent = metadata.build_sha; + document.getElementById('osType').textContent = metadata.os_type; + document.getElementById('osRelease').textContent = metadata.os_release; + + $('#metadataModal').modal('show'); + }) + .catch(error => { + console.error('Failed to fetch metadata:', error); + Swal.fire({ + icon: 'error', + title: 'Erreur', + text: 'Impossible de récupérer les métadonnées' + }); + }); + } + document.addEventListener('DOMContentLoaded', () => { + // Recherche avec debounce + const searchInput = document.getElementById('searchInput'); + if (searchInput) { + let timeout; + searchInput.addEventListener('input', (e) => { + clearTimeout(timeout); + timeout = setTimeout(() => { + const term = e.target.value.toLowerCase(); + document.querySelectorAll('#fileTable tbody tr').forEach(row => { + const text = row.querySelector('td:first-child').textContent.toLowerCase(); + row.style.display = text.includes(term) ? '' : 'none'; + }); + }, 150); + }); + } + + // Gestion des modales + document.querySelectorAll('[data-toggle="modal"]').forEach(trigger => { + trigger.addEventListener('click', () => { + const modal = document.querySelector(trigger.dataset.target); + if (modal) modal.classList.add('show'); + }); + }); + + document.querySelectorAll('.modal .close, .modal .btn-secondary').forEach(btn => { + btn.addEventListener('click', () => { + const modal = btn.closest('.modal'); + if (modal) modal.classList.remove('show'); + }); + }); + + // Dropdowns + document.querySelectorAll('.dropdown-toggle').forEach(toggle => { + toggle.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + const menu = toggle.nextElementSibling; + if (!menu) return; + + // Fermer les autres dropdowns + document.querySelectorAll('.dropdown-menu.show').forEach(m => { + if (m !== menu) m.classList.remove('show'); + }); + + menu.classList.toggle('show'); + }); + }); + + // Fermer les dropdowns au clic extérieur + document.addEventListener('click', () => { + document.querySelectorAll('.dropdown-menu.show').forEach(menu => { + menu.classList.remove('show'); + }); + }); + }); + + // Loading overlay + window.showLoadingState = () => { + const overlay = document.createElement('div'); + overlay.className = 'loading-overlay animate'; + overlay.innerHTML = '
'; + document.body.appendChild(overlay); + }; + + window.hideLoadingState = () => { + const overlay = document.querySelector('.loading-overlay'); + if (overlay) overlay.remove(); + }; + + // Gestion améliorée de l'état de chargement +window.showLoadingState = () => { + const overlay = document.createElement('div'); + overlay.className = 'loading-overlay'; + + const wrapper = document.createElement('div'); + wrapper.className = 'spinner-wrapper'; + + const spinner = document.createElement('div'); + spinner.className = 'loading-spinner'; + + wrapper.appendChild(spinner); + overlay.appendChild(wrapper); + document.body.appendChild(overlay); + + // Force le reflow pour démarrer l'animation + overlay.offsetHeight; + overlay.classList.add('show'); +}; + +window.hideLoadingState = () => { + const overlay = document.querySelector('.loading-overlay'); + if (overlay) { + overlay.classList.remove('show'); + overlay.addEventListener('transitionend', () => overlay.remove(), { once: true }); + } +}; + +// Animation des lignes du tableau +document.addEventListener('DOMContentLoaded', () => { + const tableRows = document.querySelectorAll('.table tr'); + tableRows.forEach((row, index) => { + row.style.setProperty('--row-index', index); + requestAnimationFrame(() => row.classList.add('show')); + }); + + // Animation du conteneur principal + const mainContainer = document.querySelector('.form-container'); + if (mainContainer) { + requestAnimationFrame(() => mainContainer.classList.add('show')); + } +}); + +// Fonction pour les transitions de page +function transitionToPage(url) { + document.body.classList.add('page-transition'); + showLoadingState(); + + setTimeout(() => { + window.location.href = url; + }, 300); +} + +// Amélioration des modales +function showModal(modalId) { + const modal = document.querySelector(modalId); + if (!modal) return; + + modal.style.display = 'flex'; + requestAnimationFrame(() => { + modal.classList.add('show'); + modal.querySelector('.modal-content')?.classList.add('show'); + }); +} + +function hideModal(modalId) { + const modal = document.querySelector(modalId); + if (!modal) return; + + modal.querySelector('.modal-content')?.classList.remove('show'); + modal.classList.remove('show'); + + modal.addEventListener('transitionend', () => { + modal.style.display = 'none'; + }, { once: true }); +} + +// État de chargement des boutons +function setButtonLoading(button, isLoading) { + if (isLoading) { + button.classList.add('loading'); + button.dataset.originalText = button.innerHTML; + button.innerHTML = ''; + } else { + button.classList.remove('loading'); + if (button.dataset.originalText) { + button.innerHTML = button.dataset.originalText; + } + } +} + +function createLoadingScreen() { + const container = document.createElement('div'); + container.className = 'initial-loading'; + const content = ` +
+
+ + + + +
+
+

Vous y êtes presque !

+

Préparation de votre espace de travail...

+

Chargement des données

+
+
+
+ `; + container.innerHTML = content; + document.body.appendChild(container); + return container; +} + +function initializeLoadingScreen() { + // Vérifier si c'est la première visite de la session + const hasSeenAnimation = sessionStorage.getItem('hasSeenLoadingAnimation'); + + if (hasSeenAnimation) { + // Si l'animation a déjà été vue, initialiser directement le contenu + const contentWrapper = document.querySelector('.content-wrapper'); + if (contentWrapper) { + contentWrapper.classList.add('loaded'); + } + return Promise.resolve(); + } + + return new Promise((resolve) => { + const loadingScreen = createLoadingScreen(); + + setTimeout(() => { + loadingScreen.classList.add('fade-out'); + loadingScreen.addEventListener('animationend', () => { + loadingScreen.remove(); + // Marquer l'animation comme vue pour cette session + sessionStorage.setItem('hasSeenLoadingAnimation', 'true'); + resolve(); + }, { once: true }); + }, 2000); + }); +} + +document.addEventListener('DOMContentLoaded', async function() { + try { + await initializeLoadingScreen(); + const contentWrapper = document.querySelector('.content-wrapper'); + if (contentWrapper) { + contentWrapper.classList.add('loaded'); + } + } catch (error) { + console.error('Erreur lors du chargement:', error); + } +}); + +fetch('/build-metadata') +.then(response => response.json()) +.then(data => { + document.getElementById('version-number').textContent = data.build_version; +}) +.catch(error => { + console.error('Error fetching version:', error); + document.getElementById('version-number').textContent = 'Version indisponible'; +}); + +// Fonction pour afficher les détails de collaboration +function showCollaborationDetails(itemName, itemType) { + const modal = $('#collaborationModal'); + const usersContainer = modal.find('.collaboration-users'); + const currentItem = { name: itemName, type: itemType }; + + // Stockage des informations de l'item actuel + modal.data('currentItem', currentItem); + + fetch(`/api/dpanel/collaboration/details/${itemType}/${itemName}`) + .then(response => response.json()) + .then(data => { + let userList = ''; + if (data.activeUsers && data.activeUsers.length > 0) { + userList = data.activeUsers.map(user => { + const avatarUrl = user.profilePicture || getDefaultAvatar(user.name); + return ` +
+ ${user.name} + + +
+ `; + }).join(''); + } else { + userList = '
Aucun utilisateur actif
'; + } + usersContainer.html(userList); + modal.modal('show'); + }) + .catch(error => { + console.error('Error:', error); + Swal.fire({ + icon: 'error', + title: 'Erreur', + text: 'Impossible de charger les détails de la collaboration' + }); + }); +} + +// Gestionnaire de recherche d'utilisateurs +function searchCollabUser(username) { + if (!username) return; + + const resultsDiv = $('#searchCollabResults'); + + fetch(`/api/dpanel/collaboration/searchuser?username=${encodeURIComponent(username)}`) + .then(response => response.json()) + .then(result => { + if (result.found) { + const avatarUrl = result.user.profilePicture || getDefaultAvatar(result.user.name); + resultsDiv.html(` +
+
+ ${result.user.name} + ${result.user.name} +
+ +
+ `); + } else { + resultsDiv.html('
Utilisateur non trouvé
'); + } + }); +} + +// Initialisation des événements de collaboration +document.addEventListener('DOMContentLoaded', function() { + const modal = $('#collaborationModal'); + + // Recherche d'utilisateurs + $('#searchCollabBtn').on('click', () => { + const username = $('#searchCollabUser').val().trim(); + searchCollabUser(username); + }); + + $('#searchCollabUser').on('keypress', (e) => { + if (e.key === 'Enter') { + const username = e.target.value.trim(); + searchCollabUser(username); + } + }); + + // Gestion de l'ajout de collaborateurs + $(document).on('click', '.add-collab-btn', function() { + const userId = $(this).data('user-id'); + const currentItem = modal.data('currentItem'); + + addCollaborator(currentItem.name, currentItem.type, userId); + }); + + // Gestion de la suppression de collaborateurs + $(document).on('click', '.remove-collab-btn', function() { + const userId = $(this).data('user-id'); + const currentItem = modal.data('currentItem'); + + removeCollaborator(currentItem.name, currentItem.type, userId); + }); + + // Désactivation de la collaboration + $('#disableCollabBtn').on('click', function() { + const currentItem = modal.data('currentItem'); + modal.modal('hide'); + toggleCollaboration(currentItem.name, currentItem.type, false); + }); +}); + +function addCollaborator(itemName, itemType, userId) { + fetch('/api/dpanel/collaboration/add', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ itemName, itemType, userId }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + showCollaborationDetails(itemName, itemType); // Rafraîchir la liste + Swal.fire({ + position: 'top-end', + icon: 'success', + title: 'Collaborateur ajouté', + showConfirmButton: false, + timer: 1500, + toast: true + }); + } + }) + .catch(error => console.error('Error:', error)); +} + +function removeCollaborator(itemName, itemType, userId) { + fetch('/api/dpanel/collaboration/remove', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ itemName, itemType, userId }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + showCollaborationDetails(itemName, itemType); // Rafraîchir la liste + Swal.fire({ + position: 'top-end', + icon: 'success', + title: 'Collaborateur retiré', + showConfirmButton: false, + timer: 1500, + toast: true + }); + } + }) + .catch(error => console.error('Error:', error)); +} + +// Fonction pour afficher les détails de collaboration +function showCollaborationDetails(itemName, itemType) { + // Récupérer le modal + const modal = $('#collaborationModal'); + const usersContainer = modal.find('.collaboration-users'); + + fetch(`/api/dpanel/collaboration/details/${itemType}/${itemName}`) + .then(response => response.json()) + .then(data => { + let userList = ''; + if (data.activeUsers && data.activeUsers.length > 0) { + userList = data.activeUsers.map(user => { + const avatarUrl = user.profilePicture || getDefaultAvatar(user.name); + return ` +
+ ${user.name} + + +
+ `; + }).join(''); + } else { + userList = '
Aucun utilisateur actif
'; + } + + usersContainer.html(userList); + + // Gestionnaire pour la recherche d'utilisateurs + const searchInput = $('#searchCollabUser'); + const searchBtn = $('#searchCollabBtn'); + const resultsDiv = $('#searchCollabResults'); + + searchBtn.off('click').on('click', () => searchCollabUser(searchInput.val().trim())); + searchInput.off('keypress').on('keypress', (e) => { + if (e.key === 'Enter') { + searchCollabUser(searchInput.val().trim()); + } + }); + + // Gestionnaire pour la désactivation de la collaboration + $('#disableCollaborationBtn').off('click').on('click', () => { + modal.modal('hide'); + toggleCollaboration(itemName, itemType, false); + }); + + // Afficher le modal + modal.modal('show'); + }) + .catch(error => { + console.error('Error fetching collaboration details:', error); + Swal.fire({ + icon: 'error', + title: 'Erreur', + text: 'Impossible de charger les détails de la collaboration' + }); + }); +} + +function searchCollabUser(username) { + if (!username) return; + + fetch(`/api/dpanel/collaboration/searchuser?username=${encodeURIComponent(username)}`) + .then(response => response.json()) + .then(result => { + const resultsDiv = $('#searchCollabResults'); + if (result.found) { + const avatarUrl = result.user.profilePicture || getDefaultAvatar(result.user.name); + resultsDiv.html(` +
+
+ ${result.user.name} + ${result.user.name} +
+ +
+ `); + } else { + resultsDiv.html('
Utilisateur non trouvé
'); + } + }); +} + +// Fonction pour gérer l'activation/désactivation de la collaboration +function toggleCollaboration(itemName, itemType, enable) { + const itemLabel = itemType === 'folder' ? 'dossier' : 'fichier'; + + const performToggle = () => { + fetch('/api/dpanel/collaboration/toggle', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + itemName, + itemType, + enable + }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + updateCollaborationUI({ + itemName, + itemType, + isCollaborative: enable, + activeUsers: data.activeUsers || [] + }); + + Swal.fire({ + position: 'top', + icon: 'success', + title: `Collaboration ${enable ? 'activée' : 'désactivée'}`, + showConfirmButton: false, + timer: 1500, + toast: true + }); + } + }) + .catch(error => { + console.error('Error:', error); + Swal.fire({ + position: 'top', + icon: 'error', + title: 'Erreur lors de la modification de la collaboration', + showConfirmButton: false, + timer: 1500, + toast: true + }); + }); + }; + + if (!enable) { + performToggle(); + } else { + Swal.fire({ + title: 'Activer la collaboration ?', + text: `D'autres utilisateurs pourront accéder à ce ${itemLabel} en temps réel.`, + icon: 'question', + showCancelButton: true, + confirmButtonText: 'Activer', + cancelButtonText: 'Annuler' + }).then((result) => { + if (result.isConfirmed) { + performToggle(); + } + }); + } +} + +// Fonction pour mettre à jour l'interface utilisateur +function updateCollaborationUI(data) { + const row = document.querySelector(`tr[data-name="${data.itemName}"]`); + if (!row) return; + + const button = row.querySelector('.toggle-collaboration-btn'); + const nameCell = row.querySelector('td:first-child'); + + if (button) { + button.setAttribute('data-is-collaborative', data.isCollaborative); + button.title = data.isCollaborative ? 'Voir les collaborateurs' : 'Activer la collaboration'; + button.classList.toggle('active', data.isCollaborative); + } + + let badge = nameCell.querySelector('.collaboration-badge'); + if (data.isCollaborative) { + if (!badge) { + badge = document.createElement('span'); + badge.className = 'ml-2 badge badge-info collaboration-badge'; + badge.innerHTML = ' '; + nameCell.querySelector('div').appendChild(badge); + } + + const countSpan = badge.querySelector('.active-users-count'); + if (data.activeUsers && data.activeUsers.length > 0) { + badge.classList.remove('badge-secondary'); + badge.classList.add('badge-info'); + countSpan.textContent = ` ${data.activeUsers.length}`; + badge.title = `Collaborateurs actifs : ${data.activeUsers.map(u => u.name).join(', ')}`; + } else { + badge.classList.remove('badge-info'); + badge.classList.add('badge-secondary'); + countSpan.textContent = ''; + badge.title = 'Aucun collaborateur actif'; + } + } else if (badge) { + badge.remove(); + } +} + +// Initialisation au chargement de la page +document.addEventListener('DOMContentLoaded', async function() { + // Initialiser WebSocket + const ws = new WebSocket(`ws://${window.location.host}`); + + ws.onmessage = function(event) { + const data = JSON.parse(event.data); + if (data.type === 'collaborationStatus') { + updateCollaborationUI(data); + } + }; + + // Charger le statut initial de collaboration pour tous les éléments + try { + const response = await fetch('/api/dpanel/collaboration/status'); + const data = await response.json(); + if (data.items) { + Object.entries(data.items).forEach(([itemId, status]) => { + // Pour chaque élément dans le fichier collaboration.json + const [type, name] = itemId.split('-'); + updateCollaborationUI({ + itemName: name, + itemType: type, + isCollaborative: status.isCollaborative, + activeUsers: status.activeUsers + }); + }); + } + } catch (error) { + console.error('Error loading collaboration status:', error); + } + + // Gestionnaire pour les boutons de collaboration + document.querySelectorAll('.toggle-collaboration-btn').forEach(button => { + button.addEventListener('click', function(e) { + e.preventDefault(); + const itemName = this.getAttribute('data-item-name'); + const itemType = this.getAttribute('data-item-type'); + const isCollaborative = this.getAttribute('data-is-collaborative') === 'true'; + + if (isCollaborative) { + showCollaborationDetails(itemName, itemType); + } else { + toggleCollaboration(itemName, itemType, !isCollaborative); + } + }); + }); + + // Joindre automatiquement si on est sur un élément collaboratif + const currentPath = window.location.pathname; + const matches = currentPath.match(/\/folder\/(.+)$/); + if (matches) { + const folderName = decodeURIComponent(matches[1]); + joinCollaboration(folderName, 'folder'); + } +}); + +// Initialisation au chargement de la page +document.addEventListener('DOMContentLoaded', function() { + // Initialiser WebSocket + const ws = new WebSocket(`ws://${window.location.host}`); + + ws.onmessage = function(event) { + const data = JSON.parse(event.data); + if (data.type === 'collaborationStatus') { + updateCollaborationUI(data); + } + }; + + // Gestionnaire pour les boutons de collaboration + document.querySelectorAll('.toggle-collaboration-btn').forEach(button => { + button.addEventListener('click', function(e) { + e.preventDefault(); + const itemName = this.getAttribute('data-item-name'); + const itemType = this.getAttribute('data-item-type'); + const isCollaborative = this.getAttribute('data-is-collaborative') === 'true'; + + if (isCollaborative) { + showCollaborationDetails(itemName, itemType); + } else { + toggleCollaboration(itemName, itemType, !isCollaborative); + } + }); + }); +}); + +document.addEventListener('DOMContentLoaded', function() { + const contextMenu = document.querySelector('.context-menu'); + let selectedItem = null; + + // Activer le menu contextuel sur les lignes du tableau + document.querySelectorAll('#fileTable tbody tr').forEach(row => { + row.addEventListener('contextmenu', function(e) { + e.preventDefault(); // Empêcher le menu contextuel par défaut + + selectedItem = { + type: this.dataset.type, + name: this.dataset.name, + isCollaborative: this.querySelector('.toggle-collaboration-btn')?.dataset.isCollaborative === 'true' + }; + + // Ajuster la visibilité des options du menu en fonction du type + adjustMenuOptions(selectedItem); + + // Positionner le menu contextuel + showContextMenu(e.pageX, e.pageY); + }); + }); + + // Fermer le menu au clic en dehors + document.addEventListener('click', function(e) { + if (!contextMenu.contains(e.target)) { + hideContextMenu(); + } + }); + + // Gérer les actions du menu contextuel + document.querySelectorAll('.context-menu .menu-item').forEach(item => { + item.addEventListener('click', function(e) { + e.preventDefault(); + const action = this.dataset.action; + + if (selectedItem) { + handleMenuAction(action, selectedItem); + } + + hideContextMenu(); + }); + }); + + // Fonction pour ajuster les options du menu selon le type d'élément + function adjustMenuOptions(item) { + const menuItems = contextMenu.querySelectorAll('.menu-item'); + + menuItems.forEach(menuItem => { + const action = menuItem.dataset.action; + + // Gérer la visibilité des options selon le type + switch(action) { + case 'open': + menuItem.style.display = item.type.includes('folder') ? 'flex' : 'none'; + break; + case 'collaborate': + menuItem.style.display = item.type === 'folder' ? 'flex' : 'none'; + menuItem.querySelector('span').textContent = + item.isCollaborative ? 'Gérer la collaboration' : 'Activer la collaboration'; + break; + case 'share': + menuItem.style.display = item.type === 'file' ? 'flex' : 'none'; + break; + // Vous pouvez ajouter d'autres cas selon vos besoins + } + }); + } + + // Fonction pour afficher le menu contextuel + function showContextMenu(x, y) { + const menu = contextMenu; + menu.style.display = 'block'; + + // Ajuster la position si le menu dépasse de la fenêtre + const menuRect = menu.getBoundingClientRect(); + const windowWidth = window.innerWidth; + const windowHeight = window.innerHeight; + + if (x + menuRect.width > windowWidth) { + x = windowWidth - menuRect.width; + } + + if (y + menuRect.height > windowHeight) { + y = windowHeight - menuRect.height; + } + + menu.style.left = `${x}px`; + menu.style.top = `${y}px`; + } + + // Fonction pour cacher le menu contextuel + function hideContextMenu() { + contextMenu.style.display = 'none'; + } + + // Fonction pour gérer les actions du menu + function handleMenuAction(action, item) { + switch(action) { + case 'open': + if (item.type === 'folder') { + window.location.href = `/dpanel/dashboard/folder/${encodeURIComponent(item.name)}`; + } + break; + + case 'rename': + Swal.fire({ + title: 'Renommer', + input: 'text', + inputValue: item.name, + showCancelButton: true, + confirmButtonText: 'Renommer', + cancelButtonText: 'Annuler', + inputValidator: (value) => { + if (!value) { + return 'Le nom ne peut pas être vide'; + } + } + }).then((result) => { + if (result.isConfirmed) { + // Appeler votre API de renommage ici + console.log(`Renommer ${item.name} en ${result.value}`); + } + }); + break; + + case 'delete': + Swal.fire({ + title: 'Êtes-vous sûr ?', + text: `Voulez-vous vraiment supprimer "${item.name}" ?`, + icon: 'warning', + showCancelButton: true, + confirmButtonColor: 'hsl(var(--destructive))', + confirmButtonText: 'Supprimer', + cancelButtonText: 'Annuler' + }).then((result) => { + if (result.isConfirmed) { + // Appeler votre API de suppression ici + console.log(`Supprimer ${item.name}`); + } + }); + break; + + case 'collaborate': + if (item.isCollaborative) { + showCollaborationDetails(item.name, item.type); + } else { + toggleCollaboration(item.name, item.type, true); + } + break; + + case 'move': + // Implémenter la logique de déplacement + console.log(`Déplacer ${item.name}`); + break; + + case 'share': + if (item.type === 'file') { + // Copier le lien de partage dans le presse-papier + const shareButton = document.querySelector(`[data-file-name="${item.name}"] .copy-button`); + const fileUrl = shareButton?.dataset.fileUrl; + if (fileUrl) { + navigator.clipboard.writeText(fileUrl) + .then(() => { + Swal.fire({ + position: 'top-end', + icon: 'success', + title: 'Lien copié !', + showConfirmButton: false, + timer: 1500, + toast: true + }); + }); + } + } + break; + } + } +}); + + +document.addEventListener('DOMContentLoaded', function() { + // Gestion de la suppression des dossiers + document.querySelectorAll('.delete-folder-btn').forEach(button => { + button.addEventListener('click', function(e) { + e.preventDefault(); + const folderName = this.dataset.folderName; + + Swal.fire({ + title: 'Êtes-vous sûr ?', + text: `Voulez-vous vraiment supprimer le dossier "${folderName}" ?`, + icon: 'warning', + showCancelButton: true, + confirmButtonColor: '#dc3545', + confirmButtonText: 'Supprimer', + cancelButtonText: 'Annuler' + }).then((result) => { + if (result.isConfirmed) { + // Appel à votre API pour supprimer le dossier + fetch(`/api/dpanel/folders/delete/${encodeURIComponent(folderName)}`, { + method: 'DELETE' + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + // Supprimer la ligne du tableau + const row = button.closest('tr'); + row.remove(); + + // Afficher une notification de succès + Swal.fire({ + position: 'top-end', + icon: 'success', + title: 'Dossier supprimé !', + showConfirmButton: false, + timer: 1500, + toast: true + }); + } else { + throw new Error(data.message || 'Erreur lors de la suppression'); + } + }) + .catch(error => { + Swal.fire({ + position: 'top-end', + icon: 'error', + title: 'Erreur lors de la suppression', + text: error.message, + showConfirmButton: false, + timer: 3000, + toast: true + }); + }); + } + }); + }); + }); + + // Gestion du renommage des dossiers + document.querySelectorAll('.rename-folder-btn').forEach(button => { + button.addEventListener('click', function(e) { + e.preventDefault(); + const folderName = this.dataset.folderName; + + Swal.fire({ + title: 'Renommer le dossier', + input: 'text', + inputValue: folderName, + showCancelButton: true, + confirmButtonText: 'Renommer', + cancelButtonText: 'Annuler', + inputValidator: (value) => { + if (!value) { + return 'Veuillez entrer un nom de dossier'; + } + } + }).then((result) => { + if (result.isConfirmed) { + const newName = result.value; + // Appel à votre API pour renommer le dossier + fetch('/api/dpanel/folders/rename', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + oldName: folderName, + newName: newName + }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + // Mettre à jour l'affichage + const row = button.closest('tr'); + row.querySelector('td:first-child').textContent = newName; + + // Mettre à jour les attributs data + row.dataset.name = newName; + button.dataset.folderName = newName; + button.dataset.itemName = newName; + + // Mettre à jour le lien d'ouverture + const openLink = row.querySelector('a.dropdown-item'); + if (openLink) { + openLink.href = `/dpanel/dashboard/folder/${encodeURIComponent(newName)}`; + } + + Swal.fire({ + position: 'top-end', + icon: 'success', + title: 'Dossier renommé !', + showConfirmButton: false, + timer: 1500, + toast: true + }); + } else { + throw new Error(data.message || 'Erreur lors du renommage'); + } + }) + .catch(error => { + Swal.fire({ + position: 'top-end', + icon: 'error', + title: 'Erreur lors du renommage', + text: error.message, + showConfirmButton: false, + timer: 3000, + toast: true + }); + }); + } + }); + }); + }); +}); + +document.querySelectorAll('tr[data-type="folder"] td:first-child, tr[data-type="shared-folder"] td:first-child').forEach(cell => { + cell.addEventListener('dblclick', (e) => { + e.preventDefault(); // Prevent text selection + const row = cell.closest('tr'); + const url = row.dataset.url; + if (url) { + window.location.href = url; + } + }); +}); + +// Disable text selection on double-click for folder names +document.querySelectorAll('tr[data-type="folder"] td:first-child, tr[data-type="shared-folder"] td:first-child').forEach(cell => { + cell.style.userSelect = 'none'; +}); + +document.addEventListener('DOMContentLoaded', () => { + const searchContainer = document.querySelector('.flex.justify-between.items-center'); + const button = document.createElement('button'); + button.className = 'btn btn-secondary ml-2'; + button.innerHTML = ''; + searchContainer.appendChild(button); + + const table = document.querySelector('#fileTable tbody'); + let isGridView = localStorage.getItem('isGridView') === 'true'; + + function buildGridLayout(tr) { + const firstCell = tr.querySelector('td:first-child'); + const name = firstCell.querySelector('div').innerText.trim(); + const icon = firstCell.querySelector('i').cloneNode(true); + const type = tr.querySelector('td:nth-child(2)').innerText.trim(); + const owner = tr.querySelector('td:nth-child(3)').innerText.trim(); + const size = tr.querySelector('td:nth-child(4)').innerText.trim(); + const collab = firstCell.querySelector('.collaboration-badge'); + + const content = document.createElement('div'); + content.className = 'icon-container'; + content.innerHTML = ` +
${icon.outerHTML}
+
${name}
+
${type} • ${size}
+ `; + + firstCell.innerHTML = ''; + firstCell.appendChild(content); + if (collab) { + firstCell.appendChild(collab); + } + } + + function toggleView(e) { + e?.preventDefault(); + isGridView = !table.classList.contains('grid-view'); + + if (isGridView) { + table.classList.add('grid-view'); + table.querySelectorAll('tr').forEach(buildGridLayout); + button.innerHTML = ''; + } else { + table.classList.remove('grid-view'); + location.reload(); + button.innerHTML = ''; + } + + localStorage.setItem('isGridView', isGridView); + } + + button.addEventListener('click', toggleView); + + if (isGridView) { + table.classList.add('grid-view'); + table.querySelectorAll('tr').forEach(buildGridLayout); + button.innerHTML = ''; + } +}); + +// Améliorations de la vue grille +function toggleGridView() { + const table = document.querySelector('#fileTable tbody'); + const isGridView = table.classList.contains('grid-view'); + + if (!isGridView) { + table.classList.add('grid-view'); + document.querySelectorAll('#fileTable tbody tr').forEach(tr => { + const firstCell = tr.querySelector('td:first-child'); + const icon = firstCell.querySelector('i').cloneNode(true); + const name = firstCell.textContent.trim(); + const type = tr.querySelector('td:nth-child(2)').textContent.trim(); + + const container = document.createElement('div'); + container.className = 'icon-container'; + container.innerHTML = ` +
${icon.outerHTML}
+
${name}
+
${type}
+ `; + + firstCell.innerHTML = ''; + firstCell.appendChild(container); + }); + } else { + table.classList.remove('grid-view'); + location.reload(); // Recharger pour restaurer la vue liste + } + + localStorage.setItem('viewMode', isGridView ? 'list' : 'grid'); +} + +// Gestionnaire du menu contextuel amélioré +function handleContextMenu(e) { + e.preventDefault(); + + const target = e.target.closest('tr'); + if (!target) return; + + const contextMenu = document.querySelector('.context-menu'); + contextMenu.style.display = 'block'; + + // Positionnement intelligent du menu + const x = e.pageX; + const y = e.pageY; + const winWidth = window.innerWidth; + const winHeight = window.innerHeight; + const menuWidth = contextMenu.offsetWidth; + const menuHeight = contextMenu.offsetHeight; + + contextMenu.style.left = (x + menuWidth > winWidth ? winWidth - menuWidth : x) + 'px'; + contextMenu.style.top = (y + menuHeight > winHeight ? winHeight - menuHeight : y) + 'px'; + + // Stockage de la référence à l'élément + contextMenu.dataset.targetName = target.dataset.name; + contextMenu.dataset.targetType = target.dataset.type; + + // Mise à jour des actions du menu + updateContextMenuActions(target); +} + +function updateContextMenuActions(target) { + const contextMenu = document.querySelector('.context-menu'); + const actions = contextMenu.querySelectorAll('.menu-item'); + + actions.forEach(action => { + action.onclick = (e) => { + e.preventDefault(); + const name = contextMenu.dataset.targetName; + const type = contextMenu.dataset.targetType; + + switch(action.dataset.action) { + case 'open': + if (type === 'folder') { + window.location.href = `/dpanel/dashboard/folder/${encodeURIComponent(name)}`; + } + break; + case 'rename': + if (type === 'folder') { + renameFolder(name); + } else { + renameFile(name); + } + break; + case 'delete': + if (type === 'folder') { + confirmDeleteFolder(name); + } else { + confirmDelete(name); + } + break; + case 'share': + if (type === 'file') { + const url = target.dataset.url; + copyFileLink(url); + } + break; + } + + contextMenu.style.display = 'none'; + }; + }); +} + +// Initialisation +document.addEventListener('DOMContentLoaded', () => { + // Restaurer la vue précédente + const viewMode = localStorage.getItem('viewMode'); + if (viewMode === 'grid') { + toggleGridView(); + } + + // Gestionnaire du bouton de vue + document.querySelector('.view-toggle').addEventListener('click', toggleGridView); + + // Gestionnaire du menu contextuel + document.addEventListener('contextmenu', handleContextMenu); + document.addEventListener('click', (e) => { + if (!e.target.closest('.context-menu')) { + document.querySelector('.context-menu').style.display = 'none'; + } + }); +}); + +function getDefaultAvatar(username) { + const encodedName = encodeURIComponent(username); + return `https://api.dicebear.com/7.x/initials/svg?seed=${encodedName}&background=%234e54c8&radius=50`; +} + +// Modifiez la fonction qui affiche les détails de collaboration +function showCollaborationDetails(itemName, itemType) { + fetch(`/api/dpanel/collaboration/details/${itemType}/${itemName}`) + .then(response => response.json()) + .then(data => { + let userList = ''; + if (data.activeUsers && data.activeUsers.length > 0) { + userList = data.activeUsers.map(user => { + const avatarUrl = user.profilePicture || getDefaultAvatar(user.name); + return ` +
+ ${user.name} + ${user.name} +
+ `; + }).join(''); + } else { + userList = '
Aucun utilisateur actif
'; + } + }); +} + +document.addEventListener('DOMContentLoaded', () => { + // Récupération des éléments nécessaires + const tableBody = document.querySelector('#fileTable tbody'); + const searchContainer = document.querySelector('.flex.justify-between.items-center'); + + if (!tableBody || !searchContainer) return; // Protection contre les éléments manquants + + // Création du bouton de basculement + const viewToggleBtn = document.createElement('button'); + viewToggleBtn.className = 'btn btn-secondary ml-2 view-toggle-btn'; + viewToggleBtn.innerHTML = ''; + searchContainer.appendChild(viewToggleBtn); + + // Récupération du mode de vue sauvegardé + let isGridView = localStorage.getItem('isGridView') === 'true'; + + function buildGridLayout(tr) { + if (!tr) return; + + const firstCell = tr.querySelector('td:first-child'); + if (!firstCell) return; + + const name = firstCell.innerText.trim(); + const icon = firstCell.querySelector('i')?.cloneNode(true); + const type = tr.querySelector('td:nth-child(2)')?.innerText.trim() || ''; + const owner = tr.querySelector('td:nth-child(3)')?.innerText.trim() || ''; + const size = tr.querySelector('td:nth-child(4)')?.innerText.trim() || ''; + const collab = firstCell.querySelector('.collaboration-badge'); + + if (!icon) return; + + const content = document.createElement('div'); + content.className = 'icon-container'; + content.innerHTML = ` +
${icon.outerHTML}
+
${name}
+
${type} • ${size}
+ `; + + firstCell.innerHTML = ''; + firstCell.appendChild(content); + if (collab) { + firstCell.appendChild(collab); + } + } + + function toggleView() { + isGridView = !tableBody.classList.contains('grid-view'); + + if (isGridView) { + tableBody.classList.add('grid-view'); + document.querySelectorAll('#fileTable tbody tr').forEach(buildGridLayout); + viewToggleBtn.innerHTML = ''; + } else { + tableBody.classList.remove('grid-view'); + location.reload(); // Recharge la page pour restaurer la vue liste + viewToggleBtn.innerHTML = ''; + } + + localStorage.setItem('isGridView', isGridView); + } + + // Ajout du gestionnaire d'événements + viewToggleBtn.addEventListener('click', toggleView); + + // Initialisation de la vue au chargement + if (isGridView) { + tableBody.classList.add('grid-view'); + document.querySelectorAll('#fileTable tbody tr').forEach(buildGridLayout); + viewToggleBtn.innerHTML = ''; + } +}); + +// ...existing code... + +// Gestion des collaborations (remplacez ou ajoutez cette partie) +function showCollaborationModal(itemName, itemType) { + const modalHtml = ` +
+
+
Collaborateurs actifs
+
+
+ Chargement... +
+
+
+
+
Ajouter un collaborateur
+
+ +
+ +
+
+
+
+
+ `; + + Swal.fire({ + title: 'Gestion de la collaboration', + html: modalHtml, + showCancelButton: true, + showDenyButton: true, + confirmButtonText: 'Fermer', + denyButtonText: 'Désactiver la collaboration', + denyButtonColor: '#dc3545', + width: '600px', + didOpen: () => { + // Charger les utilisateurs actifs + loadActiveUsers(itemName, itemType); + + // Configuration de la recherche + const searchInput = Swal.getPopup().querySelector('#searchUserInput'); + const searchBtn = Swal.getPopup().querySelector('#searchUserBtn'); + + searchBtn.addEventListener('click', () => searchUsers(searchInput.value, itemName, itemType)); + searchInput.addEventListener('keypress', (e) => { + if (e.key === 'Enter') { + searchUsers(searchInput.value, itemName, itemType); + } + }); + } + }).then((result) => { + if (result.isDenied) { + disableCollaboration(itemName, itemType); + } + }); +} + +// Fonction de chargement des utilisateurs actifs +function loadActiveUsers(itemName, itemType) { + fetch(`/api/dpanel/collaboration/users/${itemType}/${itemName}`) + .then(response => response.json()) + .then(data => { + const usersList = Swal.getPopup().querySelector('#activeUsersList'); + if (!usersList) return; + + if (data.users && data.users.length > 0) { + const usersHtml = data.users.map(user => ` +
+
+ ${user.name} +
+
${user.name}
+
En ligne
+
+
+ +
+ `).join(''); + usersList.innerHTML = usersHtml; + } else { + usersList.innerHTML = '
Aucun collaborateur actif
'; + } + }) + .catch(error => { + console.error('Erreur lors du chargement des utilisateurs:', error); + }); +} + +// Fonction de recherche d'utilisateurs +function searchUsers(searchTerm, itemName, itemType) { + if (!searchTerm.trim()) return; + + const resultsDiv = Swal.getPopup().querySelector('#searchResults'); + if (!resultsDiv) return; + + resultsDiv.innerHTML = '
Recherche...
'; + + fetch(`/api/dpanel/users/search?term=${encodeURIComponent(searchTerm)}`) + .then(response => response.json()) + .then(data => { + if (data.users && data.users.length > 0) { + const resultsHtml = data.users.map(user => ` +
+
+
+ ${user.name} + ${user.name} +
+ +
+
+ `).join(''); + resultsDiv.innerHTML = resultsHtml; + } else { + resultsDiv.innerHTML = '
Aucun utilisateur trouvé
'; + } + }) + .catch(error => { + console.error('Erreur lors de la recherche:', error); + resultsDiv.innerHTML = '
Erreur lors de la recherche
'; + }); +} + +// Ajoutez cet écouteur d'événements pour le bouton de collaboration +document.addEventListener('DOMContentLoaded', function() { + document.querySelectorAll('.toggle-collaboration-btn').forEach(button => { + button.addEventListener('click', function(e) { + e.preventDefault(); + const itemName = this.getAttribute('data-item-name'); + const itemType = this.getAttribute('data-item-type'); + const isCollaborative = this.getAttribute('data-is-collaborative') === 'true'; + + if (isCollaborative) { + showCollaborationModal(itemName, itemType); + } else { + toggleCollaboration(itemName, itemType, true); + } + }); + }); +}); +// Remplacez ou ajoutez cette fonction dans votre fichier dashboard.js +function showCollaborationModal(itemName, itemType) { + Swal.fire({ + title: 'Gestion de la collaboration', + html: ` +
+
+
Collaborateurs actifs
+
+
+ Chargement... +
+
+
+
+
Ajouter un collaborateur
+
+ +
+ +
+
+
+
+
+ `, + showCancelButton: true, + showDenyButton: true, + confirmButtonText: 'Fermer', + denyButtonText: 'Désactiver la collaboration', + denyButtonColor: '#dc3545', + width: '600px', + customClass: { + container: 'collaboration-modal', + popup: 'collaboration-popup', + content: 'collaboration-content' + }, + didOpen: () => { + loadActiveUsers(itemName, itemType); + setupSearchHandlers(itemName, itemType); + } + }).then((result) => { + if (result.isDenied) { + disableCollaboration(itemName, itemType); + } + }); +} + +// Modifier l'événement click des boutons de collaboration +document.addEventListener('DOMContentLoaded', function() { + document.querySelectorAll('.toggle-collaboration-btn').forEach(button => { + button.addEventListener('click', function(e) { + e.preventDefault(); + const itemName = this.getAttribute('data-item-name'); + const itemType = this.getAttribute('data-item-type'); + const isCollaborative = this.getAttribute('data-is-collaborative') === 'true'; + + if (isCollaborative) { + showCollaborationModal(itemName, itemType); + } else { + toggleCollaboration(itemName, itemType, true); + } + }); + }); +}); + +function loadActiveUsers(itemName, itemType) { + const usersList = Swal.getPopup().querySelector('#activeUsersList'); + + fetch(`/api/dpanel/collaboration/users/${itemType}/${itemName}`) + .then(response => response.json()) + .then(data => { + if (!usersList) return; + + if (data.users && data.users.length > 0) { + usersList.innerHTML = data.users.map(user => ` +
+
+ ${user.name} +
+
${user.name}
+
En ligne
+
+
+ +
+ `).join(''); + } else { + usersList.innerHTML = '
Aucun collaborateur actif
'; + } + }) + .catch(error => { + console.error('Erreur:', error); + usersList.innerHTML = '
Erreur lors du chargement
'; + }); +} + +function setupSearchHandlers(itemName, itemType) { + const searchInput = Swal.getPopup().querySelector('#searchUserInput'); + const searchBtn = Swal.getPopup().querySelector('#searchUserBtn'); + + if (!searchInput || !searchBtn) return; + + searchBtn.onclick = () => searchUsers(searchInput.value, itemName, itemType); + searchInput.onkeypress = (e) => { + if (e.key === 'Enter') { + searchUsers(searchInput.value, itemName, itemType); + } + }; +} + +function searchUsers(searchTerm, itemName, itemType) { + if (!searchTerm.trim()) return; + + const resultsDiv = Swal.getPopup().querySelector('#searchResults'); + if (!resultsDiv) return; + + resultsDiv.innerHTML = '
Recherche...
'; + + fetch(`/api/dpanel/users/search?term=${encodeURIComponent(searchTerm)}`) + .then(response => response.json()) + .then(data => { + if (data.users && data.users.length > 0) { + resultsDiv.innerHTML = data.users.map(user => ` +
+
+
+ ${user.name} + ${user.name} +
+ +
+
+ `).join(''); + } else { + resultsDiv.innerHTML = '
Aucun utilisateur trouvé
'; + } + }) + .catch(error => { + console.error('Erreur:', error); + resultsDiv.innerHTML = '
Erreur lors de la recherche
'; + }); +} \ No newline at end of file diff --git a/public/js/dashboard.js b/public/js/dashboard.js index 4a9781f..c5f0fcd 100644 --- a/public/js/dashboard.js +++ b/public/js/dashboard.js @@ -1,738 +1,1330 @@ - document.addEventListener('DOMContentLoaded', function() { - document.querySelectorAll('.file-size').forEach(function(element) { - const size = parseInt(element.getAttribute('data-size')); - element.textContent = formatFileSize(size); +// Dashboard JavaScript - Version corrigée +document.addEventListener('DOMContentLoaded', function() { + // Initialisation générale + initializeDashboard(); + initializeContextMenu(); + initializeDropdowns(); + initializeGridView(); + initializeCollaboration(); + initializeFileHandlers(); +}); + +// =================== INITIALISATION =================== +function initializeDashboard() { + // Formatage des tailles de fichiers + document.querySelectorAll('.file-size').forEach(function(element) { + const size = parseInt(element.getAttribute('data-size')); + element.textContent = formatFileSize(size); + }); + + // Événements de base + document.getElementById('searchButton')?.addEventListener('click', searchFiles); + document.getElementById('newFolderBtn')?.addEventListener('click', showNewFolderModal); + document.getElementById('themeSwitcher')?.addEventListener('click', toggleDarkMode); + + // Thème + initTheme(); + + // Version + loadVersion(); + + // Double-clic sur les dossiers + document.querySelectorAll('tr[data-type="folder"] td:first-child, tr[data-type="shared-folder"] td:first-child').forEach(cell => { + cell.style.userSelect = 'none'; + cell.addEventListener('dblclick', (e) => { + e.preventDefault(); + const row = cell.closest('tr'); + const url = row.dataset.url; + if (url) window.location.href = url; }); + }); +} - document.getElementById('searchButton').addEventListener('click', searchFiles); - document.getElementById('newFolderBtn').addEventListener('click', showNewFolderModal); +// =================== MENU CONTEXTUEL =================== +function initializeContextMenu() { + const contextMenu = document.querySelector('.context-menu'); + let selectedItem = null; - document.querySelectorAll('.delete-folder-button').forEach(button => { - button.addEventListener('click', function() { - const folderName = this.getAttribute('data-folder-name'); - confirmDeleteFolder(folderName); - }); - }); + // Gestionnaire du clic droit + document.addEventListener('contextmenu', function(e) { + const row = e.target.closest('tr[data-type]'); + if (!row) return; + + e.preventDefault(); + + selectedItem = { + type: row.dataset.type, + name: row.dataset.name, + url: row.dataset.url, + element: row + }; - document.querySelectorAll('.delete-file-button').forEach(button => { - button.addEventListener('click', function() { - const fileName = this.getAttribute('data-file-name'); - confirmDelete(fileName); - }); - }); + adjustMenuOptions(selectedItem); + showContextMenu(e.pageX, e.pageY); + }); - document.querySelectorAll('.copy-button').forEach(button => { - button.addEventListener('click', function() { - const fileUrl = this.getAttribute('data-file-url'); - copyFileLink(fileUrl); - }); - }); - - document.querySelectorAll('.rename-file-btn').forEach(button => { - button.addEventListener('click', function() { - const fileName = this.getAttribute('data-file-name'); - const folderName = this.getAttribute('data-folder-name'); - renameFile(folderName, fileName); - }); - }); - - document.querySelectorAll('.move-file-btn').forEach(button => { - button.addEventListener('click', function() { - const fileName = this.getAttribute('data-file-name'); - showMoveFileModal(fileName); - }); - }); - - document.getElementById('confirmMoveFile').addEventListener('click', moveFile); - document.getElementById('themeSwitcher').addEventListener('click', toggleDarkMode); - - initTheme(); - - document.addEventListener('DOMContentLoaded', function () { - const accountDropdownBtn = document.getElementById('accountDropdownBtn'); - const accountDropdownMenu = document.getElementById('accountDropdownMenu'); - - accountDropdownBtn.addEventListener('click', function (e) { - e.stopPropagation(); - accountDropdownMenu.classList.toggle('show'); - }); - - document.addEventListener('click', function (e) { - if (!accountDropdownBtn.contains(e.target) && !accountDropdownMenu.contains(e.target)) { - accountDropdownMenu.classList.remove('show'); - } - }); - }); - - $('.modal').modal({ - show: false - }); - - const metadataLink = document.querySelector('a[onclick="displayMetadata()"]'); - if (metadataLink) { - metadataLink.addEventListener('click', function(event) { - event.preventDefault(); - displayMetadata(); - }); + // Fermer le menu au clic extérieur + document.addEventListener('click', function(e) { + if (!contextMenu?.contains(e.target)) { + hideContextMenu(); } + }); - document.querySelectorAll('[onclick^="showFileInfo"]').forEach(link => { - link.addEventListener('click', function(event) { - event.preventDefault(); - const fileName = this.getAttribute('onclick').match(/'([^']+)'/)[1]; - showFileInfo(fileName); - }); + // Actions du menu + document.querySelectorAll('.context-menu .menu-item').forEach(item => { + item.addEventListener('click', function(e) { + e.preventDefault(); + const action = this.dataset.action; + if (selectedItem) { + handleMenuAction(action, selectedItem); + } + hideContextMenu(); }); }); - function formatFileSize(fileSizeInBytes) { - if (fileSizeInBytes < 1024) return fileSizeInBytes + ' octets'; - else if (fileSizeInBytes < 1048576) return (fileSizeInBytes / 1024).toFixed(2) + ' Ko'; - else if (fileSizeInBytes < 1073741824) return (fileSizeInBytes / 1048576).toFixed(2) + ' Mo'; - else return (fileSizeInBytes / 1073741824).toFixed(2) + ' Go'; + 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 searchFiles() { - const input = document.getElementById('searchInput'); - const filter = input.value.toUpperCase(); - const table = document.getElementById('fileTable'); - const tr = table.getElementsByTagName('tr'); + function showContextMenu(x, y) { + if (!contextMenu) return; + + contextMenu.style.display = 'block'; + + // Ajuster la position + const menuRect = contextMenu.getBoundingClientRect(); + const windowWidth = window.innerWidth; + const windowHeight = window.innerHeight; - for (let i = 1; i < tr.length; i++) { - const td = tr[i].getElementsByTagName('td')[0]; - if (td) { - const txtValue = td.textContent || td.innerText; - if (txtValue.toUpperCase().indexOf(filter) > -1) { - tr[i].style.display = ""; - } else { - tr[i].style.display = "none"; + if (x + menuRect.width > windowWidth) { + x = windowWidth - menuRect.width - 10; + } + if (y + menuRect.height > windowHeight) { + y = windowHeight - menuRect.height - 10; + } + + contextMenu.style.left = `${x}px`; + contextMenu.style.top = `${y}px`; + } + + function hideContextMenu() { + if (contextMenu) { + contextMenu.style.display = 'none'; + } + selectedItem = null; + } + + function handleMenuAction(action, item) { + switch(action) { + case 'open': + if (item.type === 'folder') { + window.location.href = `/dpanel/dashboard/folder/${encodeURIComponent(item.name)}`; + } else if (item.type === 'shared-folder') { + window.location.href = item.url; } - } + break; + + case 'rename': + if (item.type === 'folder') { + renameFolder(item.name); + } else if (item.type === 'file') { + renameFile(item.name); + } + break; + + case 'collaborate': + const collabBtn = item.element.querySelector('.toggle-collaboration-btn'); + const isCollaborative = collabBtn?.dataset.isCollaborative === 'true'; + + if (isCollaborative) { + showCollaborationDetails(item.name, item.type); + } else { + toggleCollaboration(item.name, item.type, true); + } + break; + + case 'copy-link': + if (item.url) { + navigator.clipboard.writeText(item.url).then(() => { + showToast('success', 'Lien copié'); + }); + } + break; + + case 'move': + if (item.type === 'file') { + // Utiliser la modal Bootstrap pour le déplacement + showMoveFileBootstrapModal(item.name); + } + break; + + case 'delete': + if (item.type === 'folder') { + confirmDeleteFolder(item.name); + } else if (item.type === 'file') { + confirmDeleteFile(item.name); + } + break; } } +} - function showNewFolderModal() { - Swal.fire({ - title: 'Nouveau dossier', - input: 'text', - inputPlaceholder: 'Entrer le nom du nouveau dossier', - confirmButtonText: 'Créer', - showCancelButton: true, - cancelButtonText: 'Annuler', - preConfirm: (folderName) => { - if (!folderName) { - Swal.showValidationMessage('Le nom du dossier ne peut pas être vide.'); +// =================== DROPDOWNS =================== +function initializeDropdowns() { + // Gestionnaire pour les dropdowns Bootstrap + document.querySelectorAll('[data-bs-toggle="dropdown"]').forEach(toggle => { + toggle.addEventListener('click', function(e) { + e.preventDefault(); + e.stopPropagation(); + + // Fermer les autres dropdowns + document.querySelectorAll('.dropdown-menu.show').forEach(menu => { + if (menu !== this.nextElementSibling) { + menu.classList.remove('show'); } - return folderName; - } - }).then(result => { - if (result.isConfirmed) { - createNewFolder(result.value); + }); + + // Toggle le dropdown actuel + const dropdown = this.nextElementSibling; + if (dropdown && dropdown.classList.contains('dropdown-menu')) { + dropdown.classList.toggle('show'); } }); - } + }); - function createNewFolder(folderName) { - fetch('/api/dpanel/dashboard/newfolder', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ folderName }), - }) - .then(response => { - if (response.ok) { - return response.json(); - } else { - return response.json().then(error => Promise.reject(error)); - } - }) - .then(result => { - Swal.fire({ - position: 'top', - icon: 'success', - title: 'Le dossier a été créé avec succès.', - showConfirmButton: false, - timer: 2000, - toast: true - }).then(() => { - location.reload(); - }); - }) - .catch(error => { - Swal.fire({ - position: 'top', - icon: 'error', - title: 'Erreur lors de la création du dossier.', - text: error.message, - showConfirmButton: false, - timer: 2350, - toast: true - }); + // Fermer les dropdowns au clic extérieur + document.addEventListener('click', function(e) { + if (!e.target.closest('.dropdown')) { + document.querySelectorAll('.dropdown-menu.show').forEach(menu => { + menu.classList.remove('show'); }); - } + } + }); - function confirmDeleteFolder(folderName) { - Swal.fire({ - title: 'Êtes-vous sûr?', - text: `La suppression du dossier "${folderName}" est irréversible!`, - icon: 'warning', - showCancelButton: true, - confirmButtonColor: '#d33', - cancelButtonColor: '#3085d6', - confirmButtonText: 'Supprimer', - cancelButtonText: 'Annuler', - }).then((result) => { - if (result.isConfirmed) { - deleteFolder(folderName); - } + // Empêcher la fermeture du dropdown quand on clique à l'intérieur + document.querySelectorAll('.dropdown-menu').forEach(menu => { + menu.addEventListener('click', function(e) { + e.stopPropagation(); }); - } + }); +} - function deleteFolder(folderName) { - fetch(`/api/dpanel/dashboard/deletefolder/${folderName}`, { - method: 'DELETE', - }) - .then(response => { - if (response.ok) { - Swal.fire({ - position: 'top', - icon: 'success', - title: 'Le dossier a été supprimé avec succès.', - showConfirmButton: false, - timer: 1800, - toast: true - }).then(() => { - location.reload(); - }); - } else { - throw new Error('La suppression du dossier a échoué'); - } - }) - .catch(error => { - Swal.fire({ - position: 'top', - icon: 'error', - title: error.message, - showConfirmButton: false, - timer: 1800, - toast: true +// =================== VUE GRILLE =================== +function initializeGridView() { + const tableBody = document.querySelector('#fileTable tbody'); + const searchContainer = document.querySelector('.flex.justify-between.items-center'); + + if (!tableBody || !searchContainer) return; + + // Créer le bouton de basculement + const viewToggleBtn = document.createElement('button'); + viewToggleBtn.className = 'btn btn-secondary ml-2 view-toggle-btn'; + viewToggleBtn.innerHTML = ''; + viewToggleBtn.title = 'Basculer vers la vue grille'; + searchContainer.appendChild(viewToggleBtn); + + let isGridView = localStorage.getItem('isGridView') === 'true'; function buildGridLayout(tr) { + if (!tr) return; + + const firstCell = tr.querySelector('td:first-child'); + if (!firstCell) return; + + const nameDiv = firstCell.querySelector('div'); + if (!nameDiv) return; + + const name = nameDiv.textContent.trim(); + const icon = firstCell.querySelector('i')?.cloneNode(true); + const type = tr.querySelector('td:nth-child(2)')?.textContent.trim() || ''; + const size = tr.querySelector('td:nth-child(4)')?.textContent.trim() || ''; + const collab = firstCell.querySelector('.collaboration-badge'); + + if (!icon) return; + + // Créer le conteneur principal + const content = document.createElement('div'); + content.className = 'icon-container'; + + // Créer l'icône avec arrière-plan stylé + const iconWrapper = document.createElement('div'); + iconWrapper.className = 'icon'; + iconWrapper.appendChild(icon); + + // Créer le label + const label = document.createElement('div'); + label.className = 'label'; + label.textContent = name; + + // Créer les détails + const details = document.createElement('div'); + details.className = 'details'; + details.textContent = size !== '-' ? `${type} • ${size}` : type; + + // Assembler le conteneur + content.appendChild(iconWrapper); + content.appendChild(label); + content.appendChild(details); + + // Nettoyer et remplacer le contenu + firstCell.innerHTML = ''; + firstCell.appendChild(content); + + // Réajouter le badge de collaboration s'il existe + if (collab) { + const clonedCollab = collab.cloneNode(true); + firstCell.appendChild(clonedCollab); + } + + // Ajouter le dropdown pour la vue grille + const dropdown = tr.querySelector('.dropdown'); + if (dropdown) { + const gridDropdown = dropdown.cloneNode(true); + gridDropdown.className = 'grid-dropdown dropdown'; + firstCell.appendChild(gridDropdown); + } + } function toggleView() { + const currentIsGrid = tableBody.classList.contains('grid-view'); + isGridView = !currentIsGrid; + + if (isGridView) { + // Passer à la vue grille + tableBody.classList.add('grid-view'); + + // Appliquer le layout grille avec un délai pour l'animation + setTimeout(() => { + document.querySelectorAll('#fileTable tbody tr').forEach((tr, index) => { + tr.style.animationDelay = `${index * 0.05}s`; + buildGridLayout(tr); }); - }); - } - - function confirmDelete(filename) { - Swal.fire({ - title: 'Êtes-vous sûr de vouloir supprimer ce fichier?', - text: 'Cette action est irréversible!', - icon: 'warning', - showCancelButton: true, - confirmButtonColor: '#d33', - cancelButtonColor: '#3085d6', - confirmButtonText: 'Supprimer' - }).then((result) => { - if (result.isConfirmed) { - deleteFile(filename); - } - }); - } - - function deleteFile(filename) { - fetch('/api/dpanel/dashboard/delete', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - filename: filename, - }), - }) - .then(response => { - if (response.ok) { - Swal.fire({ - position: 'top', - icon: 'success', - title: 'Le fichier a été supprimé avec succès.', - showConfirmButton: false, - timer: 1800, - toast: true - }).then(() => { - location.reload(); - }); - } else { - throw new Error('La suppression du fichier a échoué'); - } - }) - .catch(error => { - Swal.fire({ - position: 'top', - icon: 'error', - title: error.message, - showConfirmButton: false, - timer: 1800, - toast: true - }); - }); - } - - function copyFileLink(fileUrl) { - navigator.clipboard.writeText(fileUrl).then(() => { - Swal.fire({ - position: 'top', - icon: 'success', - title: 'Lien copié !', - showConfirmButton: false, - timer: 1500, - toast: true - }); - }, (err) => { - console.error('Erreur lors de la copie: ', err); - }); - } - - function renameFile(folderName, currentName) { - Swal.fire({ - title: 'Entrez le nouveau nom', - input: 'text', - inputValue: currentName, - inputPlaceholder: 'Nouveau nom', - showCancelButton: true, - confirmButtonText: 'Renommer', - cancelButtonText: 'Annuler', - inputValidator: (value) => { - if (!value) { - return 'Vous devez entrer un nom de fichier'; - } - } - }).then((result) => { - if (result.isConfirmed) { - const newName = result.value; - fetch(`/api/dpanel/dashboard/rename/${folderName}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ currentName: currentName, newName: newName }), - }) - .then(response => { - if (!response.ok) { - throw new Error('Network response was not ok'); - } - return response.json(); - }) - .then(data => { - Swal.fire({ - position: 'top', - icon: 'success', - title: 'Le fichier a été renommé avec succès.', - showConfirmButton: false, - timer: 1800, - toast: true, - }).then(() => { - location.reload(); - }); - }) - .catch((error) => { - Swal.fire({ - position: 'top', - icon: 'error', - title: 'Erreur lors du renommage du fichier.', - text: error.message, - showConfirmButton: false, - timer: 1800, - toast: true, - }); - }); - } - }); - } - - function showMoveFileModal(fileName) { - document.getElementById('moveFileName').value = fileName; - $('#moveFileModal').modal('show'); - } - - function moveFile() { - const fileName = document.getElementById('moveFileName').value; - const folderName = document.getElementById('moveFolderSelect').value; - - fetch('/api/dpanel/dashboard/movefile', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ fileName: fileName, folderName: folderName }), - }) - .then(response => { - if (!response.ok) { - throw new Error('Network response was not ok'); - } - return response.json(); - }) - .then(data => { - if (data.message === "File moved successfully") { - Swal.fire({ - position: 'top', - icon: 'success', - title: 'Le fichier a été déplacé avec succès.', - showConfirmButton: false, - timer: 1800, - toast: true, - }).then(() => { - location.reload(); - }); - } else { - throw new Error(data.error || 'Une erreur est survenue'); - } - }) - .catch((error) => { - Swal.fire({ - position: 'top', - icon: 'error', - title: 'Erreur lors du déplacement du fichier.', - text: error.message, - showConfirmButton: false, - timer: 1800, - toast: true, - }); - }); - - $('#moveFileModal').modal('hide'); - } - - const body = document.body; - const themeSwitcher = document.getElementById('themeSwitcher'); - - function setTheme(theme) { - if (theme === 'dark') { - body.classList.add('dark'); + }, 50); + + viewToggleBtn.innerHTML = ''; + viewToggleBtn.title = 'Basculer vers la vue liste'; } else { - body.classList.remove('dark'); + // Retourner à la vue liste + tableBody.classList.remove('grid-view'); + + // Rafraîchir pour restaurer la vue liste originale + setTimeout(() => location.reload(), 100); } - localStorage.setItem('theme', theme); + + localStorage.setItem('isGridView', isGridView); } + viewToggleBtn.addEventListener('click', toggleView); // Initialiser la vue si nécessaire + if (isGridView) { + tableBody.classList.add('grid-view'); + + // Appliquer le layout avec animation délayée + setTimeout(() => { + document.querySelectorAll('#fileTable tbody tr').forEach((tr, index) => { + tr.style.animationDelay = `${index * 0.05}s`; + buildGridLayout(tr); + }); + }, 100); + + viewToggleBtn.innerHTML = ''; + viewToggleBtn.title = 'Basculer vers la vue liste'; + } +} + +// =================== COLLABORATION =================== +function initializeCollaboration() { + // WebSocket pour la collaboration en temps réel + try { + const ws = new WebSocket(`ws://${window.location.host}`); + + ws.onmessage = function(event) { + const data = JSON.parse(event.data); + if (data.type === 'collaborationStatus') { + updateCollaborationUI(data); + } + }; + + ws.onerror = function(error) { + console.warn('WebSocket error:', error); + }; + } catch (error) { + console.warn('WebSocket not available:', error); + } + + // Charger le statut initial + loadCollaborationStatus(); + + // Gestionnaires d'événements + document.querySelectorAll('.toggle-collaboration-btn').forEach(button => { + button.addEventListener('click', function(e) { + e.preventDefault(); + const itemName = this.getAttribute('data-item-name'); + const itemType = this.getAttribute('data-item-type'); + const isCollaborative = this.getAttribute('data-is-collaborative') === 'true'; + + if (isCollaborative) { + showCollaborationDetails(itemName, itemType); + } else { + toggleCollaboration(itemName, itemType, true); + } + }); + }); +} + +async function loadCollaborationStatus() { + try { + const response = await fetch('/api/dpanel/collaboration/status'); + const data = await response.json(); + + if (data.items) { + Object.entries(data.items).forEach(([itemId, status]) => { + const [type, name] = itemId.split('-'); + updateCollaborationUI({ + itemName: name, + itemType: type, + isCollaborative: status.isCollaborative, + activeUsers: status.activeUsers + }); + }); + } + } catch (error) { + console.error('Error loading collaboration status:', error); + } +} + +function toggleCollaboration(itemName, itemType, enable) { + const itemLabel = itemType === 'folder' ? 'dossier' : 'fichier'; + + const performToggle = () => { + fetch('/api/dpanel/collaboration/toggle', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ itemName, itemType, enable }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + updateCollaborationUI({ + itemName, + itemType, + isCollaborative: enable, + activeUsers: data.activeUsers || [] + }); + showToast('success', `Collaboration ${enable ? 'activée' : 'désactivée'}`); + } else { + throw new Error(data.error || 'Erreur inconnue'); + } + }) + .catch(error => { + console.error('Error:', error); + showToast('error', 'Erreur lors de la modification de la collaboration'); + }); + }; + + if (!enable) { + performToggle(); + } else { + Swal.fire({ + title: 'Activer la collaboration ?', + text: `D'autres utilisateurs pourront accéder à ce ${itemLabel} en temps réel.`, + icon: 'question', + showCancelButton: true, + confirmButtonText: 'Activer', + cancelButtonText: 'Annuler' + }).then((result) => { + if (result.isConfirmed) { + performToggle(); + } + }); + } +} + +function showCollaborationDetails(itemName, itemType) { + fetch(`/api/dpanel/collaboration/details/${itemType}/${itemName}`) + .then(response => response.json()) + .then(data => { + // Supprimer toute modal existante + document.querySelectorAll('.collaboration-modal').forEach(modal => modal.remove()); + + const modal = createCollaborationModal(itemName, itemType, data); + document.body.appendChild(modal); + + // Afficher la modal avec les classes CSS modernes + modal.classList.add('show'); + modal.style.display = 'flex'; + }) + .catch(error => { + console.error('Error:', error); + showToast('error', 'Impossible de charger les détails de la collaboration'); + }); +} + +function createCollaborationModal(itemName, itemType, data) { + const modal = document.createElement('div'); + modal.className = 'collaboration-modal'; + modal.style.display = 'flex'; + modal.innerHTML = ` + + `; + + // Gestionnaire pour fermer en cliquant à l'extérieur + modal.addEventListener('click', (e) => { + if (e.target === modal) { + modal.remove(); + } + }); + + // Ajouter les gestionnaires d'événements + const searchInput = modal.querySelector('.search-user-input'); + const searchBtn = modal.querySelector('.search-user-btn'); + + const search = () => searchCollabUser(searchInput.value.trim(), itemName, itemType, modal); + + searchBtn.addEventListener('click', search); + searchInput.addEventListener('keypress', (e) => { + if (e.key === 'Enter') search(); + }); + + return modal; +} + +function searchCollabUser(username, itemName, itemType, modal) { + if (!username) return; + + const resultsDiv = modal.querySelector('.search-results'); + resultsDiv.innerHTML = ` +
+ + Recherche en cours... +
+ `; + + fetch(`/api/dpanel/collaboration/searchuser?username=${encodeURIComponent(username)}`) + .then(response => response.json()) + .then(result => { + if (result.found) { + resultsDiv.innerHTML = ` +
+ + +
+ `; + } else { + resultsDiv.innerHTML = ` +
+ + Utilisateur non trouvé +
+ `; + } + }) + .catch(error => { + console.error('Error:', error); + resultsDiv.innerHTML = ` +
+ + Erreur lors de la recherche +
+ `; + }); +} + +function addCollaborator(itemName, itemType, userId) { + fetch('/api/dpanel/collaboration/add', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ itemName, itemType, userId }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + showToast('success', 'Collaborateur ajouté'); + // Rafraîchir la modal + document.querySelector('.collaboration-modal')?.remove(); + showCollaborationDetails(itemName, itemType); + } else { + throw new Error(data.error || 'Erreur lors de l\'ajout'); + } + }) + .catch(error => { + console.error('Error:', error); + showToast('error', 'Erreur lors de l\'ajout du collaborateur'); + }); +} + +function removeCollaborator(itemName, itemType, userId) { + fetch('/api/dpanel/collaboration/remove', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ itemName, itemType, userId }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + showToast('success', 'Collaborateur retiré'); + // Rafraîchir la modal + document.querySelector('.collaboration-modal')?.remove(); + showCollaborationDetails(itemName, itemType); + } else { + throw new Error(data.error || 'Erreur lors du retrait'); + } + }) + .catch(error => { + console.error('Error:', error); + showToast('error', 'Erreur lors du retrait du collaborateur'); + }); +} + +function updateCollaborationUI(data) { + const row = document.querySelector(`tr[data-name="${data.itemName}"]`); + if (!row) return; + + const button = row.querySelector('.toggle-collaboration-btn'); + const nameCell = row.querySelector('td:first-child'); + + if (button) { + button.setAttribute('data-is-collaborative', data.isCollaborative); + button.title = data.isCollaborative ? 'Voir les collaborateurs' : 'Activer la collaboration'; + } + + let badge = nameCell.querySelector('.collaboration-badge'); + if (data.isCollaborative) { + if (!badge) { + badge = document.createElement('span'); + badge.className = 'ml-2 badge badge-info collaboration-badge'; + badge.innerHTML = ' '; + nameCell.querySelector('div').appendChild(badge); + } + + const countSpan = badge.querySelector('.active-users-count'); + if (data.activeUsers && data.activeUsers.length > 0) { + badge.classList.remove('badge-secondary'); + badge.classList.add('badge-info'); + countSpan.textContent = ` ${data.activeUsers.length}`; + badge.title = `Collaborateurs actifs : ${data.activeUsers.map(u => u.name).join(', ')}`; + } else { + badge.classList.remove('badge-info'); + badge.classList.add('badge-secondary'); + countSpan.textContent = ''; + badge.title = 'Aucun collaborateur actif'; + } + } else if (badge) { + badge.remove(); + } +} + +// =================== GESTIONNAIRES DE FICHIERS =================== +function initializeFileHandlers() { + // Boutons de suppression + document.querySelectorAll('.delete-file-button, .delete-folder-btn').forEach(button => { + button.addEventListener('click', function(e) { + e.preventDefault(); + const fileName = this.dataset.fileName; + const folderName = this.dataset.folderName; + + if (fileName) { + confirmDeleteFile(fileName); + } else if (folderName) { + confirmDeleteFolder(folderName); + } + }); + }); + + // Boutons de copie + document.querySelectorAll('.copy-button').forEach(button => { + button.addEventListener('click', function(e) { + e.preventDefault(); + const fileUrl = this.dataset.fileUrl; + copyFileLink(fileUrl); + }); + }); + + // Boutons de renommage + document.querySelectorAll('.rename-file-btn, .rename-folder-btn').forEach(button => { + button.addEventListener('click', function(e) { + e.preventDefault(); + const fileName = this.dataset.fileName; + const folderName = this.dataset.folderName; + + if (fileName) { + renameFile(fileName); + } else if (folderName) { + renameFolder(folderName); + } + }); + }); + + // Boutons de déplacement + document.querySelectorAll('.move-file-btn').forEach(button => { + button.addEventListener('click', function(e) { + e.preventDefault(); + const fileName = this.dataset.fileName; + // Utiliser la modal Bootstrap pour le déplacement + showMoveFileBootstrapModal(fileName); + }); + }); // Gestionnaire pour le bouton confirmMoveFile dans la modal Bootstrap + const confirmMoveFileBtn = document.getElementById('confirmMoveFile'); + if (confirmMoveFileBtn) { + confirmMoveFileBtn.addEventListener('click', function(e) { + e.preventDefault(); + + const fileNameElement = document.getElementById('moveFileName'); + const folderSelectElement = document.getElementById('moveFolderSelect'); + + console.log('Elements found:', { + fileNameElement: !!fileNameElement, + folderSelectElement: !!folderSelectElement, + fileNameElementType: fileNameElement?.tagName, + folderSelectElementType: folderSelectElement?.tagName + }); + + if (!fileNameElement) { + showToast('error', 'Élément fileName non trouvé'); + return; + } + + if (!folderSelectElement) { + showToast('error', 'Élément folderSelect non trouvé'); + return; + } + // Récupérer les valeurs brutes + const rawFileName = fileNameElement.value; + const rawFolderName = folderSelectElement.value; + + console.log('Raw values:', { + rawFileName: rawFileName, + rawFolderName: rawFolderName, + rawFileNameType: typeof rawFileName, + rawFolderNameType: typeof rawFolderName, + fileNameIsString: typeof rawFileName === 'string', + folderNameIsString: typeof rawFolderName === 'string' + }); + + // Convertir explicitement en string et nettoyer + // Utiliser une méthode plus robuste de conversion + let fileName, folderName; + + try { + fileName = (rawFileName + '').trim(); // Force conversion to string + folderName = (rawFolderName + '').trim(); + } catch (e) { + console.error('Error converting values:', e); + fileName = String(rawFileName || '').trim(); + folderName = String(rawFolderName || '').trim(); + } + + console.log('Processed values:', { + fileName: fileName, + folderName: folderName, + fileNameType: typeof fileName, + folderNameType: typeof folderName, + fileNameLength: fileName.length, + folderNameLength: folderName.length, + fileNameConstructor: fileName.constructor.name, + folderNameConstructor: folderName.constructor.name + }); + + if (!fileName || fileName === '') { + showToast('error', 'Aucun fichier sélectionné'); + return; + } + + if (!folderName || folderName === '') { + showToast('error', 'Veuillez sélectionner un dossier'); + return; + } + + // Fermer la modal + $('#moveFileModal').modal('hide'); + + // Appeler la fonction moveFile + console.log('Calling moveFile with:', { fileName, folderName }); + moveFile(fileName, folderName); + }); + } +} + +// =================== FONCTIONS UTILITAIRES =================== +function formatFileSize(fileSizeInBytes) { + if (!fileSizeInBytes || fileSizeInBytes === 0) return '0 octets'; + if (fileSizeInBytes < 1024) return fileSizeInBytes + ' octets'; + else if (fileSizeInBytes < 1048576) return (fileSizeInBytes / 1024).toFixed(2) + ' Ko'; + else if (fileSizeInBytes < 1073741824) return (fileSizeInBytes / 1048576).toFixed(2) + ' Mo'; + else return (fileSizeInBytes / 1073741824).toFixed(2) + ' Go'; +} + +function getDefaultAvatar(username) { + const encodedName = encodeURIComponent(username); + return `https://api.dicebear.com/7.x/initials/svg?seed=${encodedName}&background=%234e54c8&radius=50`; +} + +function showToast(type, message) { + Swal.fire({ + position: 'top-end', + icon: type, + title: message, + showConfirmButton: false, + timer: 1500, + toast: true + }); +} + +function searchFiles() { + const input = document.getElementById('searchInput'); + const filter = input.value.toUpperCase(); + const table = document.getElementById('fileTable'); + const tr = table.getElementsByTagName('tr'); + + for (let i = 1; i < tr.length; i++) { + const td = tr[i].getElementsByTagName('td')[0]; + if (td) { + const txtValue = td.textContent || td.innerText; + if (txtValue.toUpperCase().indexOf(filter) > -1) { + tr[i].style.display = ""; + } else { + tr[i].style.display = "none"; + } + } + } +} + +// =================== MODALES ET ACTIONS =================== +function showNewFolderModal() { + Swal.fire({ + title: 'Nouveau dossier', + input: 'text', + inputPlaceholder: 'Entrer le nom du nouveau dossier', + confirmButtonText: 'Créer', + showCancelButton: true, + cancelButtonText: 'Annuler', + preConfirm: (folderName) => { + if (!folderName) { + Swal.showValidationMessage('Le nom du dossier ne peut pas être vide.'); + } + return folderName; + } + }).then(result => { + if (result.isConfirmed) { + createNewFolder(result.value); + } + }); +} + +function createNewFolder(folderName) { + fetch('/api/dpanel/dashboard/newfolder', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ folderName }) + }) + .then(response => response.json()) + .then(result => { + if (result.success || response.ok) { + showToast('success', 'Dossier créé avec succès'); + setTimeout(() => location.reload(), 1500); + } else { + throw new Error(result.message || 'Erreur lors de la création'); + } + }) + .catch(error => { + console.error('Error:', error); + showToast('error', 'Erreur lors de la création du dossier'); + }); +} + +function confirmDeleteFolder(folderName) { + Swal.fire({ + title: 'Êtes-vous sûr?', + text: `La suppression du dossier "${folderName}" est irréversible!`, + icon: 'warning', + showCancelButton: true, + confirmButtonColor: '#d33', + cancelButtonColor: '#3085d6', + confirmButtonText: 'Supprimer', + cancelButtonText: 'Annuler', + }).then((result) => { + if (result.isConfirmed) { + deleteFolder(folderName); + } + }); +} + +function deleteFolder(folderName) { + fetch(`/api/dpanel/dashboard/deletefolder/${folderName}`, { + method: 'DELETE' + }) + .then(response => { + if (response.ok) { + showToast('success', 'Dossier supprimé avec succès'); + setTimeout(() => location.reload(), 1500); + } else { + throw new Error('La suppression du dossier a échoué'); + } + }) + .catch(error => { + console.error('Error:', error); + showToast('error', 'Erreur lors de la suppression du dossier'); + }); +} + +function confirmDeleteFile(filename) { + Swal.fire({ + title: 'Êtes-vous sûr de vouloir supprimer ce fichier?', + text: 'Cette action est irréversible!', + icon: 'warning', + showCancelButton: true, + confirmButtonColor: '#d33', + cancelButtonColor: '#3085d6', + confirmButtonText: 'Supprimer' + }).then((result) => { + if (result.isConfirmed) { + deleteFile(filename); + } + }); +} + +function deleteFile(filename) { + fetch('/api/dpanel/dashboard/delete', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ filename: filename }) + }) + .then(response => { + if (response.ok) { + showToast('success', 'Fichier supprimé avec succès'); + setTimeout(() => location.reload(), 1500); + } else { + throw new Error('La suppression du fichier a échoué'); + } + }) + .catch(error => { + console.error('Error:', error); + showToast('error', 'Erreur lors de la suppression du fichier'); + }); +} + +function copyFileLink(fileUrl) { + navigator.clipboard.writeText(fileUrl).then(() => { + showToast('success', 'Lien copié !'); + }, (err) => { + console.error('Erreur lors de la copie: ', err); + showToast('error', 'Erreur lors de la copie'); + }); +} + +function renameFile(currentName) { + Swal.fire({ + title: 'Renommer le fichier', + input: 'text', + inputValue: currentName, + inputPlaceholder: 'Nouveau nom', + showCancelButton: true, + confirmButtonText: 'Renommer', + cancelButtonText: 'Annuler', + inputValidator: (value) => { + if (!value) { + return 'Vous devez entrer un nom de fichier'; + } + } + }).then((result) => { + if (result.isConfirmed) { + const newName = result.value; + fetch(`/api/dpanel/dashboard/rename/`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ currentName: currentName, newName: newName }) + }) + .then(response => response.json()) + .then(data => { + if (data.success || response.ok) { + showToast('success', 'Fichier renommé avec succès'); + setTimeout(() => location.reload(), 1500); + } else { + throw new Error(data.message || 'Erreur lors du renommage'); + } + }) + .catch(error => { + console.error('Error:', error); + showToast('error', 'Erreur lors du renommage du fichier'); + }); + } + }); +} + +function renameFolder(currentName) { + Swal.fire({ + title: 'Renommer le dossier', + input: 'text', + inputValue: currentName, + showCancelButton: true, + confirmButtonText: 'Renommer', + cancelButtonText: 'Annuler', + inputValidator: (value) => { + if (!value) { + return 'Veuillez entrer un nom de dossier'; + } + } + }).then((result) => { + if (result.isConfirmed) { + const newName = result.value; + fetch('/api/dpanel/folders/rename', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ oldName: currentName, newName: newName }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + showToast('success', 'Dossier renommé avec succès'); + setTimeout(() => location.reload(), 1500); + } else { + throw new Error(data.message || 'Erreur lors du renommage'); + } + }) + .catch(error => { + console.error('Error:', error); + showToast('error', 'Erreur lors du renommage du dossier'); + }); + } + }); +} + +function showMoveFileModal(fileName) { + const folders = Array.from(document.querySelectorAll('tr[data-type="folder"]')) + .map(folderRow => ({ + name: folderRow.dataset.name, + value: folderRow.dataset.name + })); + + Swal.fire({ + title: 'Déplacer le fichier', + html: ` + + `, + showCancelButton: true, + confirmButtonText: 'Déplacer', + cancelButtonText: 'Annuler', + preConfirm: () => { + const select = document.getElementById('moveFolderSelect'); + const folderName = select.value; + if (!folderName) { + Swal.showValidationMessage('Veuillez sélectionner un dossier'); + return false; + } + return folderName; + } + }).then((result) => { + if (result.isConfirmed && result.value) { + moveFile(fileName, result.value); + } + }); +} + +// Fonction alternative utilisant la modal Bootstrap +function showMoveFileBootstrapModal(fileName) { + // S'assurer que fileName est une chaîne de caractères + const fileNameStr = String(fileName).trim(); + + console.log('showMoveFileBootstrapModal called with:', { + original: fileName, + converted: fileNameStr, + type: typeof fileName + }); + + if (!fileNameStr) { + showToast('error', 'Nom de fichier invalide'); + return; + } + + // Définir le nom du fichier dans le champ caché + const moveFileNameElement = document.getElementById('moveFileName'); + if (moveFileNameElement) { + moveFileNameElement.value = fileNameStr; + console.log('Set moveFileName value to:', moveFileNameElement.value); + } else { + console.error('Element moveFileName not found'); + showToast('error', 'Erreur: Élément de formulaire manquant'); + return; + } + + // Afficher la modal Bootstrap + $('#moveFileModal').modal('show'); +} + +function moveFile(fileName, folderName) { + // S'assurer que les paramètres sont des chaînes de caractères + const fileNameStr = String(fileName).trim(); + const folderNameStr = String(folderName).trim(); + + console.log('moveFile called with:', { + original: { fileName, folderName }, + converted: { fileNameStr, folderNameStr }, + types: { + originalFileName: typeof fileName, + originalFolderName: typeof folderName, + convertedFileName: typeof fileNameStr, + convertedFolderName: typeof folderNameStr + } + }); + + if (!fileNameStr) { + showToast('error', 'Nom de fichier invalide'); + return; + } + + if (!folderNameStr) { + showToast('error', 'Nom de dossier invalide'); + return; + } + // Créer explicitement l'objet à envoyer + const requestBody = {}; + requestBody.fileName = fileNameStr; + requestBody.folderName = folderNameStr; + + // Vérification finale + console.log('Request body before sending:', { + requestBody: requestBody, + stringify: JSON.stringify(requestBody), + fileNameInBody: requestBody.fileName, + folderNameInBody: requestBody.folderName, + fileNameType: typeof requestBody.fileName, + folderNameType: typeof requestBody.folderName, + fileNameConstructor: requestBody.fileName.constructor.name, + folderNameConstructor: requestBody.folderName.constructor.name + }); + + const jsonPayload = JSON.stringify(requestBody); + console.log('JSON payload:', jsonPayload); + + fetch('/api/dpanel/dashboard/movefile', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: jsonPayload + }) + .then(response => response.json()) + .then(data => { + if (data.message === "File moved successfully" || data.success) { + showToast('success', 'Fichier déplacé avec succès'); + setTimeout(() => location.reload(), 1500); + } else { + throw new Error(data.error || 'Une erreur est survenue'); + } + }) + .catch(error => { + console.error('Error:', error); + showToast('error', 'Erreur lors du déplacement du fichier'); + }); +} + +// =================== THÈME =================== +function initTheme() { + const body = document.body; const savedTheme = localStorage.getItem('theme'); + if (savedTheme) { setTheme(savedTheme); } else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { setTheme('dark'); } - - themeSwitcher.addEventListener('click', function() { - if (body.classList.contains('dark')) { - setTheme('light'); - } else { - setTheme('dark'); - } - }); - - async function showFileInfo(fileName) { - try { - const response = await fetch('/api/dpanel/dashboard/getmetadatafile/file_info', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - fileLink: fileName, - }) - }); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - const data = await response.json(); - const fileInfo = data.find(file => file.fileName === fileName); - - if (!fileInfo) { - throw new Error(`No information found for the file ${fileName}.`); - } - - let html = `

Nom du fichier : ${fileInfo.fileName}

`; - if (fileInfo.expiryDate) { - html += `

Date d'expiration : ${fileInfo.expiryDate}

`; - } - if (fileInfo.password) { - html += `

Mot de passe : Oui

`; - } - if (fileInfo.userId) { - html += `

Utilisateur : ${fileInfo.userId}

`; - } - - Swal.fire({ - title: 'Informations sur le fichier', - html: html, - confirmButtonText: 'Fermer' - }); - } catch (error) { - console.error('Error in showFileInfo:', error); - Swal.fire({ - position: 'top', - icon: 'error', - title: 'Les informations sur le fichier ne sont pas disponibles pour le moment.', - text: `Erreur : ${error.message}`, - showConfirmButton: false, - timer: 1800, - toast: true, - }); - } - } - - function displayMetadata() { - fetch('/build-metadata') - .then(response => response.json()) - .then(metadata => { - document.getElementById('buildVersion').textContent = metadata.build_version; - document.getElementById('nodeVersion').textContent = metadata.node_version; - document.getElementById('expressVersion').textContent = metadata.express_version; - document.getElementById('buildSha').textContent = metadata.build_sha; - document.getElementById('osType').textContent = metadata.os_type; - document.getElementById('osRelease').textContent = metadata.os_release; - - $('#metadataModal').modal('show'); - }) - .catch(error => { - console.error('Failed to fetch metadata:', error); - Swal.fire({ - icon: 'error', - title: 'Erreur', - text: 'Impossible de récupérer les métadonnées' - }); - }); - } - document.addEventListener('DOMContentLoaded', () => { - // Recherche avec debounce - const searchInput = document.getElementById('searchInput'); - if (searchInput) { - let timeout; - searchInput.addEventListener('input', (e) => { - clearTimeout(timeout); - timeout = setTimeout(() => { - const term = e.target.value.toLowerCase(); - document.querySelectorAll('#fileTable tbody tr').forEach(row => { - const text = row.querySelector('td:first-child').textContent.toLowerCase(); - row.style.display = text.includes(term) ? '' : 'none'; - }); - }, 150); - }); - } - - // Gestion des modales - document.querySelectorAll('[data-toggle="modal"]').forEach(trigger => { - trigger.addEventListener('click', () => { - const modal = document.querySelector(trigger.dataset.target); - if (modal) modal.classList.add('show'); - }); - }); - - document.querySelectorAll('.modal .close, .modal .btn-secondary').forEach(btn => { - btn.addEventListener('click', () => { - const modal = btn.closest('.modal'); - if (modal) modal.classList.remove('show'); - }); - }); - - // Dropdowns - document.querySelectorAll('.dropdown-toggle').forEach(toggle => { - toggle.addEventListener('click', (e) => { - e.preventDefault(); - e.stopPropagation(); - const menu = toggle.nextElementSibling; - if (!menu) return; - - // Fermer les autres dropdowns - document.querySelectorAll('.dropdown-menu.show').forEach(m => { - if (m !== menu) m.classList.remove('show'); - }); - - menu.classList.toggle('show'); - }); - }); - - // Fermer les dropdowns au clic extérieur - document.addEventListener('click', () => { - document.querySelectorAll('.dropdown-menu.show').forEach(menu => { - menu.classList.remove('show'); - }); - }); - }); - - // Loading overlay - window.showLoadingState = () => { - const overlay = document.createElement('div'); - overlay.className = 'loading-overlay animate'; - overlay.innerHTML = '
'; - document.body.appendChild(overlay); - }; - - window.hideLoadingState = () => { - const overlay = document.querySelector('.loading-overlay'); - if (overlay) overlay.remove(); - }; - - // Gestion améliorée de l'état de chargement -window.showLoadingState = () => { - const overlay = document.createElement('div'); - overlay.className = 'loading-overlay'; - - const wrapper = document.createElement('div'); - wrapper.className = 'spinner-wrapper'; - - const spinner = document.createElement('div'); - spinner.className = 'loading-spinner'; - - wrapper.appendChild(spinner); - overlay.appendChild(wrapper); - document.body.appendChild(overlay); - - // Force le reflow pour démarrer l'animation - overlay.offsetHeight; - overlay.classList.add('show'); -}; - -window.hideLoadingState = () => { - const overlay = document.querySelector('.loading-overlay'); - if (overlay) { - overlay.classList.remove('show'); - overlay.addEventListener('transitionend', () => overlay.remove(), { once: true }); - } -}; - -// Animation des lignes du tableau -document.addEventListener('DOMContentLoaded', () => { - const tableRows = document.querySelectorAll('.table tr'); - tableRows.forEach((row, index) => { - row.style.setProperty('--row-index', index); - requestAnimationFrame(() => row.classList.add('show')); - }); - - // Animation du conteneur principal - const mainContainer = document.querySelector('.form-container'); - if (mainContainer) { - requestAnimationFrame(() => mainContainer.classList.add('show')); - } -}); - -// Fonction pour les transitions de page -function transitionToPage(url) { - document.body.classList.add('page-transition'); - showLoadingState(); - - setTimeout(() => { - window.location.href = url; - }, 300); } -// Amélioration des modales -function showModal(modalId) { - const modal = document.querySelector(modalId); - if (!modal) return; - - modal.style.display = 'flex'; - requestAnimationFrame(() => { - modal.classList.add('show'); - modal.querySelector('.modal-content')?.classList.add('show'); - }); -} - -function hideModal(modalId) { - const modal = document.querySelector(modalId); - if (!modal) return; - - modal.querySelector('.modal-content')?.classList.remove('show'); - modal.classList.remove('show'); - - modal.addEventListener('transitionend', () => { - modal.style.display = 'none'; - }, { once: true }); -} - -// État de chargement des boutons -function setButtonLoading(button, isLoading) { - if (isLoading) { - button.classList.add('loading'); - button.dataset.originalText = button.innerHTML; - button.innerHTML = ''; +function setTheme(theme) { + const body = document.body; + if (theme === 'dark') { + body.classList.add('dark'); } else { - button.classList.remove('loading'); - if (button.dataset.originalText) { - button.innerHTML = button.dataset.originalText; - } + body.classList.remove('dark'); + } + localStorage.setItem('theme', theme); +} + +function toggleDarkMode() { + const body = document.body; + if (body.classList.contains('dark')) { + setTheme('light'); + } else { + setTheme('dark'); } } -function createLoadingScreen() { - const container = document.createElement('div'); - container.className = 'initial-loading'; - const content = ` -
-
- - - - -
-
-

Vous y êtes presque !

-

Préparation de votre espace de travail...

-

Chargement des données

-
-
-
- `; - container.innerHTML = content; - document.body.appendChild(container); - return container; +// =================== VERSION =================== +function loadVersion() { + fetch('/build-metadata') + .then(response => response.json()) + .then(data => { + const versionElement = document.getElementById('version-number'); + if (versionElement) { + versionElement.textContent = data.build_version; + } + }) + .catch(error => { + console.error('Error fetching version:', error); + const versionElement = document.getElementById('version-number'); + if (versionElement) { + versionElement.textContent = 'Version indisponible'; + } + }); } -function initializeLoadingScreen() { - // Vérifier si c'est la première visite de la session - const hasSeenAnimation = sessionStorage.getItem('hasSeenLoadingAnimation'); - - if (hasSeenAnimation) { - // Si l'animation a déjà été vue, initialiser directement le contenu - const contentWrapper = document.querySelector('.content-wrapper'); - if (contentWrapper) { - contentWrapper.classList.add('loaded'); - } - return Promise.resolve(); - } +// =================== METADATA =================== +function displayMetadata() { + fetch('/build-metadata') + .then(response => response.json()) + .then(metadata => { + document.getElementById('buildVersion').textContent = metadata.build_version; + document.getElementById('nodeVersion').textContent = metadata.node_version; + document.getElementById('expressVersion').textContent = metadata.express_version; + document.getElementById('buildSha').textContent = metadata.build_sha; + document.getElementById('osType').textContent = metadata.os_type; + document.getElementById('osRelease').textContent = metadata.os_release; - return new Promise((resolve) => { - const loadingScreen = createLoadingScreen(); - - setTimeout(() => { - loadingScreen.classList.add('fade-out'); - loadingScreen.addEventListener('animationend', () => { - loadingScreen.remove(); - // Marquer l'animation comme vue pour cette session - sessionStorage.setItem('hasSeenLoadingAnimation', 'true'); - resolve(); - }, { once: true }); - }, 2000); - }); + // Utiliser Bootstrap modal si disponible, sinon créer une modal simple + const modalElement = document.getElementById('metadataModal'); + if (modalElement && typeof $ !== 'undefined' && $.fn.modal) { + $('#metadataModal').modal('show'); + } + }) + .catch(error => { + console.error('Failed to fetch metadata:', error); + showToast('error', 'Impossible de récupérer les métadonnées'); + }); } -document.addEventListener('DOMContentLoaded', async function() { - try { - await initializeLoadingScreen(); - const contentWrapper = document.querySelector('.content-wrapper'); - if (contentWrapper) { - contentWrapper.classList.add('loaded'); - } - } catch (error) { - console.error('Erreur lors du chargement:', error); - } -}); - -fetch('/build-metadata') -.then(response => response.json()) -.then(data => { - document.getElementById('version-number').textContent = data.build_version; -}) -.catch(error => { - console.error('Error fetching version:', error); - document.getElementById('version-number').textContent = 'Version indisponible'; -}); \ No newline at end of file +// Expose les fonctions globalement si nécessaire +window.displayMetadata = displayMetadata; +window.addCollaborator = addCollaborator; +window.removeCollaborator = removeCollaborator; +window.toggleCollaboration = toggleCollaboration; diff --git a/public/js/profile.script.js b/public/js/profile.script.js new file mode 100644 index 0000000..a21b73b --- /dev/null +++ b/public/js/profile.script.js @@ -0,0 +1,268 @@ +document.addEventListener('DOMContentLoaded', function() { + initTheme(); + initTabs(); + initForms(); + updateTokenDisplay(); +}); + +// Gestion du thème +function initTheme() { + const body = document.body; + const themeSwitcher = document.getElementById('themeSwitcher'); + + function setTheme(theme) { + if (theme === 'dark') { + body.classList.add('dark'); + } else { + body.classList.remove('dark'); + } + localStorage.setItem('theme', theme); + updateThemeIcon(theme); + } + + function updateThemeIcon(theme) { + const icon = themeSwitcher.querySelector('i'); + icon.className = theme === 'dark' ? 'fas fa-moon' : 'fas fa-sun'; + } + + const savedTheme = localStorage.getItem('theme'); + if (savedTheme) { + setTheme(savedTheme); + } else if (window.matchMedia('(prefers-color-scheme: dark)').matches) { + setTheme('dark'); + } + + themeSwitcher.addEventListener('click', () => { + setTheme(body.classList.contains('dark') ? 'light' : 'dark'); + }); +} + +// Gestion des onglets +function initTabs() { + const tabs = document.querySelectorAll('.tab'); + tabs.forEach(tab => { + tab.addEventListener('click', () => switchTab(tab)); + }); +} + +function switchTab(selectedTab) { + document.querySelectorAll('.tab, .tab-content').forEach(el => { + el.classList.remove('active'); + }); + + selectedTab.classList.add('active'); + const targetContent = document.querySelector(`[data-tab-content="${selectedTab.dataset.tab}"]`); + targetContent.classList.add('active'); +} + +// Gestion des formulaires et actions +function initForms() { + const profileCustomizationForm = document.getElementById('profileCustomization'); + + if (profileCustomizationForm) { + profileCustomizationForm.addEventListener('submit', handleProfileCustomizationUpdate); + } +} + +async function handleProfileCustomizationUpdate(e) { + e.preventDefault(); + const wallpaperUrl = document.getElementById('wallpaperUrl').value; + const profilePictureUrl = document.getElementById('profilePictureUrl').value; + + try { + let promises = []; // Mise à jour du fond d'écran si l'URL est fournie + if (wallpaperUrl && wallpaperUrl.trim()) { + promises.push( + fetch('/api/dpanel/dashboard/backgroundcustom/wallpaper', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + wallpaperUrl: wallpaperUrl + }) + }) + ); + } + + // Mise à jour de la photo de profil si l'URL est fournie + if (profilePictureUrl && profilePictureUrl.trim()) { + promises.push( + fetch('/api/dpanel/dashboard/profilpicture', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + profilePictureUrl: profilePictureUrl + }) + }) + ); + } + + if (promises.length === 0) { + showToast('error', 'Veuillez remplir au moins un champ'); + return; + } + + // Attendre toutes les requêtes + const responses = await Promise.all(promises); + // Vérifier toutes les réponses + let wallpaperData = null; + let profilePictureData = null; + + for (let i = 0; i < responses.length; i++) { + const response = responses[i]; + + // Vérifier le type de contenu + const contentType = response.headers.get('content-type'); + if (!contentType || !contentType.includes('application/json')) { + throw new Error('Le serveur a renvoyé une réponse non-JSON. Vérifiez votre authentification.'); + } + + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.error || `Erreur ${response.status}: ${response.statusText}`); + } + + // Identifier quelle réponse correspond à quoi + if (data.wallpaper) { + wallpaperData = data; + } else if (data.profilePicture) { + profilePictureData = data; + } + } + + // Appliquer les changements visuels + if (wallpaperData) { + document.body.style.backgroundImage = `url('${wallpaperData.wallpaper}')`; + } + + if (profilePictureData) { + const profileImages = document.querySelectorAll('.avatar-container img, .profile-picture img, .user-avatar'); + profileImages.forEach(img => { + img.src = profilePictureData.profilePicture; + }); + } + + showToast('success', 'Profil mis à jour avec succès'); + + // Vider les champs après la mise à jour réussie + document.getElementById('wallpaperUrl').value = ''; + document.getElementById('profilePictureUrl').value = ''; + + } catch (error) { + showToast('error', error.message); + } +} + +// Fonctions utilitaires +function maskToken(token) { + if (!token || token === 'Aucun token') return 'Aucun token'; + // Prendre les 8 premiers caractères et remplacer le reste par des points + return token.substring(0, 8) + '••••••••••••'; +} + +function updateTokenDisplay() { + const tokenDisplay = document.querySelector('.token-display code'); + const fullToken = tokenDisplay.getAttribute('data-full-token'); + + if (!fullToken || fullToken === 'Aucun token') { + tokenDisplay.textContent = 'Aucun token'; + } else { + // Afficher la version masquée mais conserver le token complet dans data-full-token + tokenDisplay.textContent = maskToken(fullToken); + } +} + +function copyToken() { + const tokenElement = document.querySelector('.token-display code'); + const fullToken = tokenElement.getAttribute('data-full-token'); // Récupère le token complet depuis l'attribut data-full-token + + if (!fullToken || fullToken === 'Aucun token') { + showToast('error', 'Aucun token à copier'); + return; + } + + // Copier le token complet, pas le texte affiché + navigator.clipboard.writeText(fullToken); + showToast('success', 'Token complet copié !'); +} + +async function regenerateToken() { + const userName = document.querySelector('meta[name="user-name"]').content; + const userId = document.querySelector('meta[name="user-id"]').content; + + const result = await Swal.fire({ + title: 'Êtes-vous sûr ?', + text: "Cette action va générer un nouveau token", + icon: 'warning', + showCancelButton: true, + confirmButtonColor: '#3085d6', + cancelButtonColor: '#d33', + confirmButtonText: 'Oui, continuer', + cancelButtonText: 'Annuler' + }); + + if (result.isConfirmed) { + try { + const response = await fetch('/api/dpanel/generate-token', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + name: userName, + id: userId + }) + }); + + const data = await response.json(); + + if (data.token) { + const tokenDisplay = document.querySelector('.token-display code'); + // D'abord stocker le token complet dans l'attribut + tokenDisplay.setAttribute('data-full-token', data.token); + // Ensuite afficher la version masquée + tokenDisplay.textContent = maskToken(data.token); + + // Mettre à jour les boutons + const securityContent = document.querySelector('[data-tab-content="security"] .settings-card-content .flex'); + securityContent.innerHTML = ` + + + `; + + // Copier le token complet + navigator.clipboard.writeText(data.token); + + Swal.fire({ + title: 'Token généré avec succès', + text: 'Le token complet a été copié dans votre presse-papiers.', + icon: 'success', + confirmButtonText: 'OK' + }); + } else { + throw new Error(data.error || 'Erreur lors de la génération du token'); + } + } catch (error) { + Swal.fire('Erreur', error.message, 'error'); + } + } +} + +function showToast(icon, title) { + Swal.fire({ + icon, + title, + toast: true, + position: 'top-end', + showConfirmButton: false, + timer: 3000 + }); +} diff --git a/routes/Dpanel/API/Collaboration.js b/routes/Dpanel/API/Collaboration.js new file mode 100644 index 0000000..2937aee --- /dev/null +++ b/routes/Dpanel/API/Collaboration.js @@ -0,0 +1,358 @@ +const express = require('express'); +const router = express.Router(); +const path = require('path'); +const fs = require('fs').promises; +const { logger } = require('../../../config/logs'); + +const collaborationFilePath = path.join(__dirname, '../../../data', 'collaboration.json'); + +// Fonction utilitaire pour lire le fichier de collaboration +async function readCollaborationFile() { + try { + const exists = await fs.access(collaborationFilePath) + .then(() => true) + .catch(() => false); + + if (!exists) { + await fs.writeFile(collaborationFilePath, JSON.stringify({ activeFiles: {} })); + return { activeFiles: {} }; + } + + const data = await fs.readFile(collaborationFilePath, 'utf8'); + return JSON.parse(data); + } catch (error) { + logger.error('Error reading collaboration file:', error); + return { activeFiles: {} }; + } +} + +// Fonction utilitaire pour écrire dans le fichier de collaboration +async function writeCollaborationFile(data) { + try { + await fs.writeFile(collaborationFilePath, JSON.stringify(data, null, 2)); + return true; + } catch (error) { + logger.error('Error writing collaboration file:', error); + return false; + } +} + +router.post('/toggle', async (req, res) => { + try { + const { itemName, itemType, enable } = req.body; + + // Lire ou créer le fichier de collaboration + let collaborationData = await readCollaborationFile(); + + // S'assurer que la structure est correcte + if (!collaborationData.activeFiles) { + collaborationData.activeFiles = {}; + } + + // Mettre à jour le statut de collaboration + const itemId = `${itemType}-${itemName}`; + if (enable) { + collaborationData.activeFiles[itemId] = { + name: itemName, + type: itemType, + isCollaborative: true, + activeUsers: [], + lastModified: new Date().toISOString() + }; + } else { + delete collaborationData.activeFiles[itemId]; + } + + // Sauvegarder les changements + await writeCollaborationFile(collaborationData); + + // Notifier via WebSocket si disponible + if (req.app.get('wsManager')) { + req.app.get('wsManager').broadcast({ + type: 'collaborationStatus', + itemName, + itemType, + isCollaborative: enable, + activeUsers: collaborationData.activeFiles[itemId]?.activeUsers || [] + }); + } + + res.json({ + success: true, + isCollaborative: enable, + activeUsers: collaborationData.activeFiles[itemId]?.activeUsers || [] + }); + + } catch (error) { + logger.error('Error in collaboration toggle:', error); + res.status(500).json({ + success: false, + error: 'Internal server error' + }); + } +}); + +// Route pour obtenir le statut d'un fichier +router.get('/file-status/:fileId', async (req, res) => { + try { + const { fileId } = req.params; + const collaboration = await readCollaborationFile(); + + res.json({ + fileId, + isCollaborative: true, + activeUsers: collaboration.activeFiles[fileId]?.activeUsers || [], + lastModified: collaboration.activeFiles[fileId]?.lastModified || null + }); + } catch (error) { + logger.error('Error getting file status:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +router.get('/searchuser', async (req, res) => { + try { + const { username } = req.query; + const userFilePath = path.join(__dirname, '../../../data', 'user.json'); + const users = JSON.parse(await fs.readFile(userFilePath, 'utf8')); + + const user = users.find(u => u.name.toLowerCase() === username.toLowerCase()); + + if (user) { + res.json({ + found: true, + user: { + id: user.id, + name: user.name, + profilePicture: user.profilePicture + } + }); + } else { + res.json({ found: false }); + } + } catch (error) { + logger.error('Error searching user:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +// Route pour rejoindre un fichier +router.post('/join', async (req, res) => { + try { + const { itemName, itemType, userId } = req.body; + const collaboration = await readCollaborationFile(); + + const itemId = `${itemType}-${itemName}`; + + if (!collaboration.activeFiles[itemId]) { + collaboration.activeFiles[itemId] = { + name: itemName, + type: itemType, + isCollaborative: true, + activeUsers: [], + lastModified: new Date().toISOString() + }; + } + + // Ajouter l'utilisateur si pas déjà présent + if (!collaboration.activeFiles[itemId].activeUsers.find(u => u.id === userId)) { + // Récupérer les infos de l'utilisateur + const userFilePath = path.join(__dirname, '../../../data', 'user.json'); + const users = JSON.parse(await fs.readFile(userFilePath, 'utf8')); + const user = users.find(u => u.id === userId); + + if (user) { + collaboration.activeFiles[itemId].activeUsers.push({ + id: user.id, + name: user.name, + profilePicture: user.profilePicture, + lastActive: new Date().toISOString() + }); + + await writeCollaborationFile(collaboration); + + // Notifier via WebSocket + if (req.app.get('wsManager')) { + req.app.get('wsManager').broadcast({ + type: 'collaborationStatus', + itemName, + itemType, + isCollaborative: true, + activeUsers: collaboration.activeFiles[itemId].activeUsers + }); + } + } + } + + res.json({ success: true }); + } catch (error) { + logger.error('Error in join collaboration:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +// Route pour ajouter un collaborateur +router.post('/add', async (req, res) => { + try { + const { itemName, itemType, userId } = req.body; + const collaboration = await readCollaborationFile(); + + const itemId = `${itemType}-${itemName}`; + + if (!collaboration.activeFiles[itemId]) { + return res.status(404).json({ error: 'Item not found in collaboration' }); + } + + // Vérifier si l'utilisateur n'est pas déjà dans la liste + if (!collaboration.activeFiles[itemId].activeUsers.find(u => u.id === userId)) { + // Récupérer les infos de l'utilisateur + const userFilePath = path.join(__dirname, '../../../data', 'user.json'); + const users = JSON.parse(await fs.readFile(userFilePath, 'utf8')); + const user = users.find(u => u.id === userId); + + if (user) { + collaboration.activeFiles[itemId].activeUsers.push({ + id: user.id, + name: user.name, + profilePicture: user.profilePicture, + lastActive: new Date().toISOString() + }); + + await writeCollaborationFile(collaboration); + + // Notifier via WebSocket + if (req.app.get('wsManager')) { + req.app.get('wsManager').broadcast({ + type: 'collaborationStatus', + itemName, + itemType, + isCollaborative: true, + activeUsers: collaboration.activeFiles[itemId].activeUsers + }); + } + + res.json({ success: true }); + } else { + res.status(404).json({ error: 'User not found' }); + } + } else { + res.status(400).json({ error: 'User already in collaboration' }); + } + } catch (error) { + logger.error('Error adding collaborator:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +// Route pour retirer un collaborateur +router.post('/remove', async (req, res) => { + try { + const { itemName, itemType, userId } = req.body; + const collaboration = await readCollaborationFile(); + + const itemId = `${itemType}-${itemName}`; + + if (!collaboration.activeFiles[itemId]) { + return res.status(404).json({ error: 'Item not found in collaboration' }); + } + + // Retirer l'utilisateur de la liste + collaboration.activeFiles[itemId].activeUsers = + collaboration.activeFiles[itemId].activeUsers.filter(user => user.id !== userId); + + await writeCollaborationFile(collaboration); + + // Notifier via WebSocket + if (req.app.get('wsManager')) { + req.app.get('wsManager').broadcast({ + type: 'collaborationStatus', + itemName, + itemType, + isCollaborative: true, + activeUsers: collaboration.activeFiles[itemId].activeUsers + }); + } + + res.json({ success: true }); + } catch (error) { + logger.error('Error removing collaborator:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +// Route pour quitter un fichier +router.post('/leave', async (req, res) => { + try { + const { fileId } = req.body; + const userId = req.user.id; + + if (!fileId || !userId) { + return res.status(400).json({ error: 'FileId and userId are required' }); + } + + const collaboration = await readCollaborationFile(); + + if (collaboration.activeFiles[fileId]) { + collaboration.activeFiles[fileId].activeUsers = + collaboration.activeFiles[fileId].activeUsers.filter(user => user.id !== userId); + + if (collaboration.activeFiles[fileId].activeUsers.length === 0) { + delete collaboration.activeFiles[fileId]; + } + + await writeCollaborationFile(collaboration); + if (req.app.get('wsManager')) { + req.app.get('wsManager').broadcastFileStatus(fileId); + } + } + + res.json({ success: true }); + } catch (error) { + logger.error('Error leaving file:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +router.get('/status', async (req, res) => { + try { + const collaboration = await readCollaborationFile(); + res.json({ items: collaboration.activeFiles }); + } catch (error) { + logger.error('Error getting collaboration status:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +// Route pour obtenir les détails d'un élément collaboratif +router.get('/details/:type/:name', async (req, res) => { + try { + const { type, name } = req.params; + const collaboration = await readCollaborationFile(); + const itemId = `${type}-${name}`; + + res.json(collaboration.activeFiles[itemId] || { + isCollaborative: false, + activeUsers: [] + }); + } catch (error) { + logger.error('Error getting collaboration details:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +// Route pour obtenir les utilisateurs actifs d'un élément +router.get('/users/:type/:name', async (req, res) => { + try { + const { type, name } = req.params; + const collaboration = await readCollaborationFile(); + const itemId = `${type}-${name}`; + + const activeUsers = collaboration.activeFiles[itemId]?.activeUsers || []; + res.json({ users: activeUsers }); + } catch (error) { + logger.error('Error getting active users:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/routes/Dpanel/API/MoveFile.js b/routes/Dpanel/API/MoveFile.js index 434bbb6..bf962c3 100644 --- a/routes/Dpanel/API/MoveFile.js +++ b/routes/Dpanel/API/MoveFile.js @@ -141,13 +141,48 @@ router.get('/', (req, res) => { }); router.post('/', authenticateToken, async (req, res) => { + console.log('MoveFile API - Raw request body:', req.body); + console.log('MoveFile API - Request body keys:', Object.keys(req.body)); + const fileName = req.body.fileName; const folderName = req.body.folderName; - if (!fileName || fileName.trim() === '') { + console.log('MoveFile API - Received data:', { + fileName: fileName, + folderName: folderName, + typeOfFileName: typeof fileName, + typeOfFolderName: typeof folderName, + fileNameStringified: JSON.stringify(fileName), + folderNameStringified: JSON.stringify(folderName), + fullBody: req.body + }); + + // Forcer la conversion en string si ce sont des objets + let finalFileName = fileName; + let finalFolderName = folderName; + + if (typeof fileName === 'object' && fileName !== null) { + console.log('fileName is an object, attempting conversion:', fileName); + finalFileName = String(fileName); + console.log('Converted fileName to:', finalFileName, typeof finalFileName); + } + + if (typeof folderName === 'object' && folderName !== null) { + console.log('folderName is an object, attempting conversion:', folderName); + finalFolderName = String(folderName); + console.log('Converted folderName to:', finalFolderName, typeof finalFolderName); + } + + if (!finalFileName || (typeof finalFileName === 'string' && finalFileName.trim() === '')) { return res.status(400).json({ error: 'No file selected for moving.' }); } + // Vérifier que folderName est une chaîne de caractères + if (finalFolderName && typeof finalFolderName !== 'string') { + console.error('folderName is not a string after conversion:', finalFolderName, typeof finalFolderName); + return res.status(400).json({ error: 'Invalid folder name format.' }); + } + try { const data = await fs.promises.readFile(path.join(__dirname, '../../../data', 'user.json'), 'utf-8'); const users = JSON.parse(data); @@ -160,21 +195,21 @@ router.post('/', authenticateToken, async (req, res) => { const userId = user.name; - if (!fileName || !userId) { - console.error('fileName or userId is undefined'); + if (!finalFileName || !userId) { + console.error('finalFileName or userId is undefined'); return res.status(500).json({ error: 'Error moving the file.' }); } - const sourcePath = path.join('cdn-files', userId, fileName); + const sourcePath = path.join('cdn-files', userId, finalFileName); let destinationDir; - if (folderName && folderName.trim() !== '') { - destinationDir = path.join('cdn-files', userId, folderName); + if (finalFolderName && finalFolderName.trim() !== '') { + destinationDir = path.join('cdn-files', userId, finalFolderName); } else { destinationDir = path.join('cdn-files', userId); } - const destinationPath = path.join(destinationDir, fileName); + const destinationPath = path.join(destinationDir, finalFileName); if (!destinationPath.startsWith(path.join('cdn-files', userId))) { return res.status(403).json({ error: 'Unauthorized: Cannot move file outside of user directory.' }); diff --git a/routes/Dpanel/API/RenameFolder.js b/routes/Dpanel/API/RenameFolder.js new file mode 100644 index 0000000..a340955 --- /dev/null +++ b/routes/Dpanel/API/RenameFolder.js @@ -0,0 +1,205 @@ +const express = require('express'); +const fs = require('fs'); +const path = require('path'); +const router = express.Router(); +const authMiddleware = require('../../../Middlewares/authMiddleware'); +const { logger, logRequestInfo, ErrorLogger, authLogger } = require('../../../config/logs'); +const bodyParser = require('body-parser'); + +router.use(bodyParser.json()); + +/** + * @swagger + * /folders/rename: + * post: + * security: + * - bearerAuth: [] + * tags: + * - Folder + * summary: Rename a folder + * description: This route allows you to rename a folder. It requires authentication. + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * oldName: + * type: string + * description: The current name of the folder + * newName: + * type: string + * description: The new name for the folder + * responses: + * 200: + * description: Success + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * message: + * type: string + * 400: + * description: Bad Request + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * message: + * type: string + * 401: + * description: Unauthorized + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * 403: + * description: Forbidden + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * message: + * type: string + * 404: + * description: Folder not found + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * message: + * type: string + * 500: + * description: Internal server error + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * message: + * type: string + */ + +router.post('/', authMiddleware, async (req, res) => { + try { + const userId = req.userData.name; + const { oldName, newName } = req.body; + + // Validation des paramètres + if (!oldName || !newName) { + return res.status(400).json({ + success: false, + message: 'Les noms de dossier ancien et nouveau sont requis.' + }); + } + + if (typeof oldName !== 'string' || typeof newName !== 'string') { + return res.status(400).json({ + success: false, + message: 'Les noms de dossier doivent être des chaînes de caractères.' + }); + } + + // Nettoyer les noms (éviter les traversées de répertoire) + const sanitizedOldName = path.basename(oldName.trim()); + const sanitizedNewName = path.basename(newName.trim()); + + if (!sanitizedOldName || !sanitizedNewName) { + return res.status(400).json({ + success: false, + message: 'Les noms de dossier ne peuvent pas être vides.' + }); + } + + // Construire les chemins + const userDir = path.join('cdn-files', userId); + const oldFolderPath = path.join(userDir, sanitizedOldName); + const newFolderPath = path.join(userDir, sanitizedNewName); + + // Vérifier que les chemins sont dans le répertoire de l'utilisateur + if (!oldFolderPath.startsWith(userDir) || !newFolderPath.startsWith(userDir)) { + ErrorLogger.error(`Unauthorized directory access attempt by user ${userId}`); + return res.status(403).json({ + success: false, + message: 'Accès non autorisé.' + }); + } + + // Vérifier que le dossier source existe + if (!fs.existsSync(oldFolderPath)) { + return res.status(404).json({ + success: false, + message: 'Le dossier à renommer n\'existe pas.' + }); + } + + // Vérifier que c'est bien un dossier + const stats = await fs.promises.stat(oldFolderPath); + if (!stats.isDirectory()) { + return res.status(400).json({ + success: false, + message: 'Le chemin spécifié n\'est pas un dossier.' + }); + } + + // Vérifier que le nouveau nom n'existe pas déjà + if (fs.existsSync(newFolderPath)) { + return res.status(400).json({ + success: false, + message: 'Un dossier avec ce nom existe déjà.' + }); + } + + // Renommer le dossier + await fs.promises.rename(oldFolderPath, newFolderPath); + + logger.info(`Folder renamed successfully by user ${userId}: ${sanitizedOldName} -> ${sanitizedNewName}`); + + res.status(200).json({ + success: true, + message: 'Dossier renommé avec succès.' + }); + + } catch (error) { + ErrorLogger.error('Error renaming folder:', error); + + if (error.code === 'ENOENT') { + return res.status(404).json({ + success: false, + message: 'Le dossier spécifié n\'existe pas.' + }); + } + + if (error.code === 'EACCES') { + return res.status(403).json({ + success: false, + message: 'Permission refusée pour renommer ce dossier.' + }); + } + + return res.status(500).json({ + success: false, + message: 'Erreur lors du renommage du dossier.' + }); + } +}); + +module.exports = router; diff --git a/routes/Dpanel/API/RevokeToken.js b/routes/Dpanel/API/RevokeToken.js new file mode 100644 index 0000000..ab50fd0 --- /dev/null +++ b/routes/Dpanel/API/RevokeToken.js @@ -0,0 +1,64 @@ +const fs = require('fs'); +const path = require('path'); +const router = require('express').Router(); + +router.post('/', (req, res) => { + if (!req.body.userId) { + return res.status(400).json({ + error: 'Bad Request. User ID is required.' + }); + } + + fs.readFile(path.join(__dirname, '../../../data', 'user.json'), 'utf8', (err, data) => { + if (err) { + console.error('Error reading user.json:', err); + return res.status(500).json({ + error: 'Internal server error while reading user data.' + }); + } + + try { + const users = JSON.parse(data); + + // Trouver l'utilisateur par ID + const userIndex = users.findIndex(u => u.id === req.body.userId); + + if (userIndex === -1) { + return res.status(404).json({ + error: 'User not found.' + }); + } + + // Supprimer le token de l'utilisateur + if (users[userIndex].token) { + delete users[userIndex].token; + } else { + return res.status(404).json({ + error: 'No API key found for this user.' + }); + } + + // Sauvegarder les modifications + fs.writeFile(path.join(__dirname, '../../../data', 'user.json'), JSON.stringify(users, null, 2), (err) => { + if (err) { + console.error('Error writing user.json:', err); + return res.status(500).json({ + error: 'Internal server error while saving user data.' + }); + } + + res.json({ + success: true, + message: 'API key successfully revoked.' + }); + }); + } catch (parseErr) { + console.error('Error parsing user.json:', parseErr); + return res.status(500).json({ + error: 'Internal server error while parsing user data.' + }); + } + }); +}); + +module.exports = router; diff --git a/routes/Dpanel/API/UserSearch.js b/routes/Dpanel/API/UserSearch.js new file mode 100644 index 0000000..f2a5761 --- /dev/null +++ b/routes/Dpanel/API/UserSearch.js @@ -0,0 +1,98 @@ +const express = require('express'); +const fs = require('fs'); +const path = require('path'); +const router = express.Router(); +const authMiddleware = require('../../../Middlewares/authMiddleware'); +const { logger } = require('../../../config/logs'); + +/** + * @swagger + * /api/dpanel/users/search: + * get: + * tags: + * - Users + * summary: Search for users by name or ID + * parameters: + * - in: query + * name: term + * required: true + * schema: + * type: string + * description: Search term (name or ID) + * - in: query + * name: username + * required: false + * schema: + * type: string + * description: Exact username to search for + * responses: + * 200: + * description: Search results + * content: + * application/json: + * schema: + * type: object + * properties: + * users: + * type: array + * items: + * type: object + * properties: + * id: + * type: string + * name: + * type: string + * profilePicture: + * type: string + * found: + * type: boolean + * user: + * type: object + * 500: + * description: Internal server error + */ + +router.get('/', authMiddleware, async (req, res) => { + try { + const { term, username } = req.query; + const userFilePath = path.join(__dirname, '../../../data', 'user.json'); + const users = JSON.parse(fs.readFileSync(userFilePath, 'utf8')); + + if (username) { + // Search for exact username match (for collaboration search) + const user = users.find(u => u.name.toLowerCase() === username.toLowerCase()); + if (user) { + res.json({ + found: true, + user: { + id: user.id, + name: user.name, + profilePicture: user.profilePicture + } + }); + } else { + res.json({ found: false }); + } + } else if (term) { + // Search for users by term (for general user search) + const searchTerm = term.toLowerCase(); + const filteredUsers = users.filter(user => + user.name.toLowerCase().includes(searchTerm) || + user.id.toLowerCase().includes(searchTerm) + ).map(user => ({ + id: user.id, + name: user.name, + profilePicture: user.profilePicture + })); + + res.json({ users: filteredUsers }); + } else { + res.status(400).json({ error: 'Search term or username required' }); + } + } catch (error) { + logger.error('Error searching users:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +module.exports = router; diff --git a/routes/Dpanel/Dashboard/index.js b/routes/Dpanel/Dashboard/index.js index 15c6af2..365f2c4 100644 --- a/routes/Dpanel/Dashboard/index.js +++ b/routes/Dpanel/Dashboard/index.js @@ -6,95 +6,146 @@ const fileUpload = require('express-fileupload'); const authMiddleware = require('../../../Middlewares/authMiddleware'); const { loggers } = require('winston'); const ncp = require('ncp').ncp; -let configFile = fs.readFileSync(path.join(__dirname, '../../../data', 'setup.json'), 'utf-8') -let config = JSON.parse(configFile)[0]; const bodyParser = require('body-parser'); const crypto = require('crypto'); const os = require('os'); const { getUserData, getSetupData } = require('../../../Middlewares/watcherMiddleware'); +let configFile = fs.readFileSync(path.join(__dirname, '../../../data', 'setup.json'), 'utf-8'); +let config = JSON.parse(configFile)[0]; let setupData = getSetupData(); let userData = getUserData(); + +// Fonction utilitaire pour lire le fichier de collaboration +async function getCollaborativeAccess(userId) { + try { + const collaborationFilePath = path.join(__dirname, '../../../data', 'collaboration.json'); + if (!fs.existsSync(collaborationFilePath)) { + return []; + } + const data = JSON.parse(await fs.promises.readFile(collaborationFilePath, 'utf8')); + + const sharedFolders = []; + + for (const [itemId, item] of Object.entries(data.activeFiles)) { + // Ne chercher que les utilisateurs qui ne sont pas le propriétaire + if (item.type === 'folder' && + item.isCollaborative && + item.activeUsers.some(u => u.id === userId)) { + + // Extraire le nom du propriétaire du dossier depuis cdn-files + const folderPath = item.name; + const ownerFolder = path.join(__dirname, '../../../cdn-files'); + + // Parcourir tous les dossiers utilisateurs + const users = await fs.promises.readdir(ownerFolder); + for (const user of users) { + const userFolderPath = path.join(ownerFolder, user, folderPath); + if (fs.existsSync(userFolderPath)) { + sharedFolders.push({ + originalPath: `${user}/${folderPath}`, + displayName: `${user}/${folderPath}`, + owner: user, + folderName: folderPath, + activeUsers: item.activeUsers + }); + break; // On a trouvé le propriétaire, on peut arrêter + } + } + } + } + return sharedFolders; + } catch (error) { + console.error('Error getting collaborative access:', error); + return []; + } +} + router.use(bodyParser.json()); router.get('/', authMiddleware, async (req, res) => { const folderName = req.params.folderName || ''; - + if (!req.userData || !req.userData.name) { return res.render('error-recovery-file', { error: 'User data is undefined or incomplete' }); } - const userId = req.userData.id; + const userId = req.userData.id; const userName = req.userData.name; const downloadDir = path.join('cdn-files', userName); const domain = config.domain || 'swiftlogic-labs.com'; - if (!config.domain) { - console.error('Domain is not defined in setup.json'); - config.domain = 'swiftlogic-labs.com'; - } - if (!fs.existsSync(downloadDir)) { fs.mkdirSync(downloadDir, { recursive: true }); } try { - fs.accessSync(downloadDir, fs.constants.R_OK | fs.constants.W_OK); - } catch (err) { - console.error('No access!', err); - return res.render('error-recovery-file', { error: 'No access to directory' }); - } - - let fileInfoNames = []; - try { - const fileInfo = JSON.parse(fs.readFileSync(path.join(__dirname, '../../../data', 'setup.json'), 'utf-8')) - - if (!Array.isArray(fileInfo)) { - console.error('fileInfo is not an array. Check the contents of file_info.json'); - } else { - fileInfoNames = fileInfo.map(file => file.fileName); - } - } catch (err) { - console.error('Error reading file_info.json:', err); - } - - try { + // Lire les fichiers du répertoire de l'utilisateur const files = await fs.promises.readdir(downloadDir); - - const folders = files.filter(file => fs.statSync(path.join(downloadDir, file)).isDirectory()); - - const fileDetails = files.map(file => { + + // Séparer les fichiers et les dossiers + const fileDetails = await Promise.all(files.map(async file => { const filePath = path.join(downloadDir, file); - const stats = fs.statSync(filePath); - const fileExtension = path.extname(file).toLowerCase(); + const stats = await fs.promises.stat(filePath); + const isDirectory = stats.isDirectory(); + const fileExtension = isDirectory ? null : path.extname(file).toLowerCase(); const encodedFileName = encodeURIComponent(file); - const fileLink = `https://${domain}/attachments/${userId}/${encodedFileName}`; - - const fileType = stats.isDirectory() ? 'folder' : 'file'; - + const fileLink = isDirectory ? null : `https://${domain}/attachments/${userId}/${encodedFileName}`; + return { name: file, size: stats.size, url: fileLink, extension: fileExtension, - type: fileType + type: isDirectory ? 'folder' : 'file', + isPersonal: true, + owner: userName, + isCollaborative: false, + activeUsers: [] }; + })); + + // Séparer les dossiers personnels et les fichiers + const personalFolders = fileDetails.filter(item => item.type === 'folder'); + const personalFiles = fileDetails.filter(item => item.type === 'file'); + + // Obtenir les dossiers partagés + const sharedFolders = await getCollaborativeAccess(userId); + + // Formater les dossiers partagés + const sharedFolderDetails = sharedFolders.map(folder => ({ + name: folder.displayName, + type: 'shared-folder', + isCollaborative: true, + originalPath: folder.originalPath, + owner: folder.owner, + folderName: folder.folderName, + activeUsers: folder.activeUsers + })); + + // Combiner tous les éléments dans l'ordre souhaité + const allItems = [ + ...personalFolders, // Dossiers personnels en premier + ...sharedFolderDetails, // Puis les dossiers partagés + ...personalFiles // Et enfin les fichiers + ]; + + const availableExtensions = Array.from(new Set( + allItems + .filter(item => item.type === 'file') + .map(file => file.extension) + )); + + res.render('dashboard', { + files: allItems, // Liste complète pour la compatibilité + folders: personalFolders, // Uniquement les dossiers personnels + extensions: availableExtensions, + allFolders: personalFolders, + folderName: folderName, + fileInfoNames: [], + userData: userData, + sharedFolders: sharedFolderDetails // Les dossiers partagés séparément }); - - function formatFileSize(fileSizeInBytes) { - if (fileSizeInBytes < 1024 * 1024) { - return `${(fileSizeInBytes / 1024).toFixed(2)} Ko`; - } else if (fileSizeInBytes < 1024 * 1024 * 1024) { - return `${(fileSizeInBytes / (1024 * 1024)).toFixed(2)} Mo`; - } else { - return `${(fileSizeInBytes / (1024 * 1024 * 1024)).toFixed(2)} Go`; - } - } - - - const availableExtensions = Array.from(new Set(fileDetails.map(file => file.extension))); - - res.render('dashboard', { files: fileDetails, folders, extensions: availableExtensions, allFolders: folders, folderName: folderName, fileInfoNames: fileInfoNames, userData: userData }); } catch (err) { console.error('Error reading directory:', err); return res.render('error-recovery-file', { error: err.message }); diff --git a/routes/Dpanel/Folder/index.js b/routes/Dpanel/Folder/index.js index 62a45fd..7b5539e 100644 --- a/routes/Dpanel/Folder/index.js +++ b/routes/Dpanel/Folder/index.js @@ -17,6 +17,90 @@ let setupData = getSetupData(); let userData = getUserData(); router.use(bodyParser.json()); +router.get('/shared/:ownerName/:folderName', authMiddleware, async (req, res) => { + const { ownerName, folderName } = req.params; + const userId = req.userData.id; + const userName = req.userData.name; + + try { + // Vérifier l'accès collaboratif + 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.some(u => u.id === userId)) { + return res.status(403).render('error-recovery-file', { + error: 'Vous n\'avez pas accès à ce dossier.' + }); + } + + // Accès au dossier partagé + const folderPath = path.join('cdn-files', ownerName, folderName); + const userFolderPath = path.join('cdn-files', ownerName); + const domain = config.domain || 'swiftlogic-labs.com'; + + // Lecture des fichiers + const entries = await fs.promises.readdir(folderPath, { withFileTypes: true }); + const allEntries = await fs.promises.readdir(userFolderPath, { withFileTypes: true }); + + const folders = entries + .filter(entry => entry.isDirectory()) + .map(entry => entry.name); + + const allFolders = allEntries + .filter(entry => entry.isDirectory()) + .map(entry => entry.name); + + // Lecture des informations de fichiers + const fileInfoData = await fs.promises.readFile( + path.join(__dirname, '../../../data', 'file_info.json'), + 'utf-8' + ); + const fileInfo = JSON.parse(fileInfoData); + const fileInfoNames = fileInfo.map(file => file.fileName); + + // Récupération des détails des fichiers + const fileDetails = await Promise.all(entries.map(async entry => { + const filePath = path.join(folderPath, entry.name); + const stats = await fs.promises.stat(filePath); + const encodedFileName = encodeURIComponent(entry.name); + const fileLink = `https://${domain}/attachments/${ownerName}/${encodedFileName}`; + + return { + name: entry.name, + size: stats.size, + url: fileLink, + extension: path.extname(entry.name).toLowerCase(), + type: entry.isDirectory() ? 'folder' : 'file' + }; + })); + + const availableExtensions = Array.from(new Set(fileDetails.map(file => file.extension))); + + res.render('folder', { + files: fileDetails, + folders, + allFolders, + extensions: availableExtensions, + currentFolder: folderName, + folderName, + fileInfoNames, + userName, + isSharedFolder: true, + ownerName + }); + + } catch (error) { + console.error('Error accessing shared folder:', error); + res.status(500).render('error-recovery-file', { + error: 'Erreur lors de l\'accès au dossier partagé' + }); + } +}); + router.get('/:folderName', authMiddleware, async (req, res) => { const userId = req.userData.name; const userName = req.userData.name; diff --git a/routes/routes.js b/routes/routes.js index b325e1f..599271d 100644 --- a/routes/routes.js +++ b/routes/routes.js @@ -11,12 +11,12 @@ const DpanelFolderRoute = require('./Dpanel/Folder/index.js'); const DpanelUploadRoute = require('./Dpanel/Upload.js'); const AttachmentsRoute = require('./attachments.js'); const buildMetadataRoute = require('./BuildMetaData.js'); -const DpanelBackgroundCustomRoute = require('./Dpanel/API/BackgroundCustom.js'); const getFileDashboardRoute = require('./Dpanel/API/getFile.js'); const getFileFolderRoute = require('./Dpanel/API/getFileFolder.js'); const swagger = require('../models/swagger.js'); const NewFolderRoute = require('./Dpanel/API/NewFolder.js'); const RenameFileRoute = require('./Dpanel/API/RenameFile.js'); +const RenameFolderRoute = require('./Dpanel/API/RenameFolder.js'); const DeleteFileRoute = require('./Dpanel/API/DeleteFile.js'); const MoveFileRoute = require('./Dpanel/API/MoveFile.js'); const UploadRoute = require('./Dpanel/API/Upload.js'); @@ -28,6 +28,8 @@ const GetMetaDataFileRoute = require('./Dpanel/API/GetMetaDataFile.js'); const BackgroundCustom = require('./Dpanel/API/BackgroundCustom.js'); const ProfilUser = require('./Dpanel/Dashboard/ProfilUser.js'); const PofilPictureRoute = require('./Dpanel/API/ProfilPicture.js'); +const CollaborationRoute = require('./Dpanel/API/Collaboration.js'); +const UserSearchRoute = require('./Dpanel/API/UserSearch.js'); const loginRoute = require('./Auth/Login.js'); const logoutRoute = require('./Auth/Logout.js'); @@ -40,6 +42,7 @@ const AdminSettingSetupDpanelRoute = require('./Dpanel/Admin/SettingSetup.js'); const AdminStatsLogsDpanelRoute = require('./Dpanel/Admin/Stats-Logs.js'); const AdminPrivacySecurityDpanelRoute = require('./Dpanel/Admin/Privacy-Security.js'); const GenerateTokenRoute = require('./Dpanel/API/GenerateToken.js'); +const RevokeTokenRoute = require('./Dpanel/API/RevokeToken.js'); router.use('/', indexRoute); router.use('/attachments', AttachmentsRoute); @@ -58,6 +61,7 @@ router.use('/dpanel/dashboard/profil', ProfilUser); router.use('/api/dpanel/dashboard/newfolder',discordWebhookSuspisiousAlertMiddleware, logApiRequest, NewFolderRoute); router.use('/api/dpanel/dashboard/rename',discordWebhookSuspisiousAlertMiddleware, logApiRequest, RenameFileRoute); +router.use('/api/dpanel/folders/rename',discordWebhookSuspisiousAlertMiddleware, logApiRequest, RenameFolderRoute); router.use('/api/dpanel/dashboard/delete',discordWebhookSuspisiousAlertMiddleware, logApiRequest, DeleteFileRoute); router.use('/api/dpanel/dashboard/movefile',discordWebhookSuspisiousAlertMiddleware, logApiRequest, MoveFileRoute); router.use('/api/dpanel/upload',discordWebhookSuspisiousAlertMiddleware, logApiRequest, UploadRoute); @@ -66,12 +70,14 @@ router.use('/api/dpanel/dashboard/admin/update-setup',discordWebhookSuspisiousAl router.use('/api/dpanel/dashboard/deletefolder',discordWebhookSuspisiousAlertMiddleware, logApiRequest, DeleteFolderRoute); router.use('/api/dpanel/dashboard/deletefile/', discordWebhookSuspisiousAlertMiddleware, logApiRequest,DeleteFileFolderRoute); router.use('/api/dpanel/dashboard/getmetadatafile',discordWebhookSuspisiousAlertMiddleware, logApiRequest, GetMetaDataFileRoute); -router.use('/api/dpanel/dashboard/backgroundcustom',discordWebhookSuspisiousAlertMiddleware, logApiRequest, DpanelBackgroundCustomRoute); +router.use('/api/dpanel/dashboard/backgroundcustom', BackgroundCustom, logApiRequest); router.use('/api/dpanel/generate-token',discordWebhookSuspisiousAlertMiddleware, logApiRequest, GenerateTokenRoute); +router.use('/api/dpanel/revoke-token',discordWebhookSuspisiousAlertMiddleware, logApiRequest, RevokeTokenRoute); router.use('/api/dpanel/dashboard/getfile', getFileDashboardRoute, logApiRequest); router.use('/api/dpanel/dashboard/getfilefolder', getFileFolderRoute, logApiRequest); -router.use('/api/dpanel/dashboard/backgroundcustom', BackgroundCustom, logApiRequest); router.use('/api/dpanel/dashboard/profilpicture', PofilPictureRoute, logApiRequest); +router.use('/api/dpanel/collaboration', discordWebhookSuspisiousAlertMiddleware, logApiRequest, CollaborationRoute); +router.use('/api/dpanel/users/search', UserSearchRoute, logApiRequest); router.use('/auth/login', loginRoute); router.use('/auth/logout', logoutRoute); diff --git a/server.js b/server.js index 0bba274..4e12672 100644 --- a/server.js +++ b/server.js @@ -37,8 +37,9 @@ const loadSetup = async () => { }; // Configuration de l'application -const configureApp = async () => { + const configureApp = async () => { const setup = await loadSetup(); + const WebSocketManager = require('./models/websocketManager.js'); // Configuration des stratégies d'authentification if (setup.discord) require('./models/Passport-Discord.js'); @@ -90,7 +91,7 @@ const configureApp = async () => { app.use((err, req, res, next) => { ErrorLogger.error('Unhandled error:', err); - res.status(500).json(response); + res.status(500).json({ error: 'Internal Server Error', message: err.message }); }); }; @@ -140,6 +141,11 @@ const startServer = () => { } }); + // Initialiser le WebSocket Manager ici, après la création du serveur + const WebSocketManager = require('./models/websocketManager.js'); + const wsManager = new WebSocketManager(server); + app.set('wsManager', wsManager); + return server; }; diff --git a/views/dashboard.ejs b/views/dashboard.ejs index 78179cd..add6ec2 100644 --- a/views/dashboard.ejs +++ b/views/dashboard.ejs @@ -29,6 +29,33 @@ + -
-
-
- - +
+ + - -
- - - - - - - - - - <% files.forEach(file => { %> - - <% if (fileInfoNames.includes(file.name)) { %> - - <% } else { %> - + +
+
+ + +
+ +
+
Nom du fichierTailleAction
<%= file.name %><%= file.name %>
+ + + + + + + + + + + + <% files.filter(item => item.type === 'folder').forEach(folder => { %> + + + + + + + + <% }); %> + + + <% if (sharedFolders && sharedFolders.length > 0) { %> + <% sharedFolders.forEach(folder => { %> + + + + + + + + <% }); %> <% } %> - - + + + + + - - <% }); %> - -
NomTypePropriétaireTailleActions
+
+ + <%= folder.name %> + <% if (folder.isCollaborative) { %> + + + <%= folder.activeUsers.length %> + + <% } %> +
+
+ Dossier + + + + moi + + - + +
+
+ + <%= folder.folderName %> + + + <%= folder.activeUsers.length %> + +
+
+ Dossier partagé + + + + <%= folder.owner === userData.name ? 'moi' : folder.owner %> + + - + +
- <% if (file.type === 'folder') { %> - Dossier - <% } else { %> - <%= file.size %> octets - <% } %> - -
- <% if (file.type === 'folder') { %> -
+
+ + <%= file.name %> +
+
+ Fichier + + + + moi + + + + <%= file.size %> octets + + + -
+ +
+ + + <% }); %> + + +
-
- Version: ... | © 2024 Myaxrin Labs + Version: ... | © Myaxrin Labs Metadata
+ +