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 0dadd90..0000000 Binary files a/public/assets/homelab_logo@2x.png and /dev/null differ diff --git a/public/assets/homelab_logo@3x.png b/public/assets/homelab_logo@3x.png deleted file mode 100644 index 0d2e462..0000000 Binary files a/public/assets/homelab_logo@3x.png and /dev/null differ 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} +
+
${user.name}
+
En ligne
+
+ +
+ `; + }).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} +
+
${user.name}
+
En ligne
+
+ +
+ `; + }).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 = ` +
+
+
+ ${result.user.name} +
+
+
${result.user.name}
+
+ + Utilisateur trouvé +
+
+
+ +
+ `; + } 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 + + + -
+ +
+ + + <% }); %> + + +
-
+ +