This commit is contained in:
@@ -1,6 +1,5 @@
|
|||||||
kind: pipeline
|
kind: pipeline
|
||||||
name: default
|
name: default
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: build-node
|
- name: build-node
|
||||||
image: node:latest
|
image: node:latest
|
||||||
@@ -11,16 +10,14 @@ steps:
|
|||||||
from_secret: git_password
|
from_secret: git_password
|
||||||
commands:
|
commands:
|
||||||
- npm install
|
- npm install
|
||||||
- node -v
|
- export VERSION=$(node -e "console.log(require('./package.json').version)")
|
||||||
|
|
||||||
|
|
||||||
- name: build-docker-image
|
- name: build-docker-image
|
||||||
image: plugins/docker
|
image: plugins/docker
|
||||||
settings:
|
settings:
|
||||||
repo: swiftlogiclabs/cdn-app-insider
|
repo: swiftlogiclabs/cdn-app-insider
|
||||||
tags:
|
tags:
|
||||||
- latest
|
- latest
|
||||||
- v1.0.0-beta.17
|
- v${VERSION}
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
username:
|
username:
|
||||||
from_secret: docker_username
|
from_secret: docker_username
|
||||||
|
|||||||
104
package-lock.json
generated
104
package-lock.json
generated
@@ -17,6 +17,7 @@
|
|||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
"chalk": "^4.1.2",
|
"chalk": "^4.1.2",
|
||||||
"chokidar": "^3.6.0",
|
"chokidar": "^3.6.0",
|
||||||
|
"compression": "^1.7.5",
|
||||||
"connect-flash": "^0.1.1",
|
"connect-flash": "^0.1.1",
|
||||||
"cookie-parser": "^1.4.6",
|
"cookie-parser": "^1.4.6",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
@@ -35,6 +36,7 @@
|
|||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"mime-types": "^2.1.35",
|
"mime-types": "^2.1.35",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
|
"multiparty": "^4.2.3",
|
||||||
"mysql2": "^3.6.3",
|
"mysql2": "^3.6.3",
|
||||||
"ncp": "^2.0.0",
|
"ncp": "^2.0.0",
|
||||||
"node-cron": "^3.0.3",
|
"node-cron": "^3.0.3",
|
||||||
@@ -1299,6 +1301,60 @@
|
|||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/compressible": {
|
||||||
|
"version": "2.0.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
|
||||||
|
"integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": ">= 1.43.0 < 2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/compression": {
|
||||||
|
"version": "1.7.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/compression/-/compression-1.7.5.tgz",
|
||||||
|
"integrity": "sha512-bQJ0YRck5ak3LgtnpKkiabX5pNF7tMUh1BSy2ZBOTh0Dim0BUu6aPPwByIns6/A5Prh8PufSPerMDUklpzes2Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bytes": "3.1.2",
|
||||||
|
"compressible": "~2.0.18",
|
||||||
|
"debug": "2.6.9",
|
||||||
|
"negotiator": "~0.6.4",
|
||||||
|
"on-headers": "~1.0.2",
|
||||||
|
"safe-buffer": "5.2.1",
|
||||||
|
"vary": "~1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/compression/node_modules/debug": {
|
||||||
|
"version": "2.6.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||||
|
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/compression/node_modules/ms": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/compression/node_modules/negotiator": {
|
||||||
|
"version": "0.6.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz",
|
||||||
|
"integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/concat-map": {
|
"node_modules/concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
@@ -3349,6 +3405,54 @@
|
|||||||
"node": ">= 6.0.0"
|
"node": ">= 6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/multiparty": {
|
||||||
|
"version": "4.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/multiparty/-/multiparty-4.2.3.tgz",
|
||||||
|
"integrity": "sha512-Ak6EUJZuhGS8hJ3c2fY6UW5MbkGUPMBEGd13djUzoY/BHqV/gTuFWtC6IuVA7A2+v3yjBS6c4or50xhzTQZImQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"http-errors": "~1.8.1",
|
||||||
|
"safe-buffer": "5.2.1",
|
||||||
|
"uid-safe": "2.1.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/multiparty/node_modules/depd": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/multiparty/node_modules/http-errors": {
|
||||||
|
"version": "1.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz",
|
||||||
|
"integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"depd": "~1.1.2",
|
||||||
|
"inherits": "2.0.4",
|
||||||
|
"setprototypeof": "1.2.0",
|
||||||
|
"statuses": ">= 1.5.0 < 2",
|
||||||
|
"toidentifier": "1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/multiparty/node_modules/statuses": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
|
||||||
|
"integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/mv": {
|
"node_modules/mv": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz",
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
"chalk": "^4.1.2",
|
"chalk": "^4.1.2",
|
||||||
"chokidar": "^3.6.0",
|
"chokidar": "^3.6.0",
|
||||||
|
"compression": "^1.7.5",
|
||||||
"connect-flash": "^0.1.1",
|
"connect-flash": "^0.1.1",
|
||||||
"cookie-parser": "^1.4.6",
|
"cookie-parser": "^1.4.6",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
@@ -36,6 +37,7 @@
|
|||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"mime-types": "^2.1.35",
|
"mime-types": "^2.1.35",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
|
"multiparty": "^4.2.3",
|
||||||
"mysql2": "^3.6.3",
|
"mysql2": "^3.6.3",
|
||||||
"ncp": "^2.0.0",
|
"ncp": "^2.0.0",
|
||||||
"node-cron": "^3.0.3",
|
"node-cron": "^3.0.3",
|
||||||
|
|||||||
BIN
public/assets/Thumbs.db
Normal file
BIN
public/assets/Thumbs.db
Normal file
Binary file not shown.
BIN
public/assets/background/Thumbs.db
Normal file
BIN
public/assets/background/Thumbs.db
Normal file
Binary file not shown.
BIN
public/assets/icon.ico
Normal file
BIN
public/assets/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.4 MiB |
@@ -321,17 +321,31 @@ position: relative !important;
|
|||||||
.initial-loading {
|
.initial-loading {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
background: linear-gradient(135deg,
|
background-color: #333;
|
||||||
hsla(var(--background), 0.85),
|
|
||||||
hsla(var(--background), 0.9)
|
|
||||||
);
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
-webkit-backdrop-filter: blur(10px);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
.initial-loading > .loader {
|
||||||
|
border: 4px solid #fff;
|
||||||
|
border-top-color: transparent;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
@keyframes spin {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
.initial-loading > .message {
|
||||||
|
margin-top: 16px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.success-animation {
|
.success-animation {
|
||||||
@@ -459,3 +473,169 @@ position: relative !important;
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Notifications container */
|
||||||
|
.notification-container {
|
||||||
|
position: fixed;
|
||||||
|
top: 1rem;
|
||||||
|
right: 1rem;
|
||||||
|
z-index: 1100;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
pointer-events: none;
|
||||||
|
max-width: 100%;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Individual notification */
|
||||||
|
.notification {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background-color: hsl(var(--card));
|
||||||
|
border: 1px solid hsl(var(--border));
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 1rem;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
transform: translateX(120%);
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||||||
|
pointer-events: auto;
|
||||||
|
max-width: 380px;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification.show {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Icon styles */
|
||||||
|
.notification-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-right: 1rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Notification types */
|
||||||
|
.notification.success .notification-icon {
|
||||||
|
background: rgba(16, 185, 129, 0.1);
|
||||||
|
color: #10B981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification.error .notification-icon {
|
||||||
|
background: rgba(239, 68, 68, 0.1);
|
||||||
|
color: #EF4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification.warning .notification-icon {
|
||||||
|
background: rgba(245, 158, 11, 0.1);
|
||||||
|
color: #F59E0B;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification.info .notification-icon {
|
||||||
|
background: rgba(59, 130, 246, 0.1);
|
||||||
|
color: #3B82F6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Content styling */
|
||||||
|
.notification-content {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.925rem;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-message {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: hsl(var(--muted-foreground));
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode adjustments */
|
||||||
|
.dark .notification {
|
||||||
|
background-color: hsl(var(--card));
|
||||||
|
border-color: hsl(var(--border));
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .notification-title {
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .notification-message {
|
||||||
|
color: hsl(var(--muted-foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animations */
|
||||||
|
@keyframes slideIn {
|
||||||
|
from {
|
||||||
|
transform: translateX(120%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideOut {
|
||||||
|
from {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(120%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* File info styles */
|
||||||
|
.file-info {
|
||||||
|
padding: 0.5rem;
|
||||||
|
background: rgba(var(--card), 0.5);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-info p {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-info strong {
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Metadata info styles */
|
||||||
|
.metadata-info {
|
||||||
|
padding: 0.5rem;
|
||||||
|
background: rgba(var(--card), 0.5);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-info p {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-info strong {
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.notification-container {
|
||||||
|
left: 1rem;
|
||||||
|
right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -715,12 +715,6 @@ function initializeLoadingScreen() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nettoyer le sessionStorage lors de la déconnexion
|
|
||||||
function handleLogout() {
|
|
||||||
sessionStorage.removeItem('hasSeenLoadingAnimation');
|
|
||||||
// Votre code de déconnexion existant...
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', async function() {
|
document.addEventListener('DOMContentLoaded', async function() {
|
||||||
try {
|
try {
|
||||||
await initializeLoadingScreen();
|
await initializeLoadingScreen();
|
||||||
|
|||||||
@@ -1,206 +1,87 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const multiparty = require('multiparty');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const fileUpload = require('express-fileupload');
|
|
||||||
const { loggers } = require('winston');
|
|
||||||
const ncp = require('ncp');
|
|
||||||
const util = require('util');
|
|
||||||
const configFile = fs.readFileSync(path.join(__dirname, '../../../data', 'setup.json'), 'utf-8')
|
|
||||||
const config = JSON.parse(configFile);
|
|
||||||
const bodyParser = require('body-parser');
|
|
||||||
const jwt = require('jsonwebtoken');
|
|
||||||
const authMiddleware = require('../../../Middlewares/authMiddleware');
|
|
||||||
const { getUserData, getSetupData } = require('../../../Middlewares/watcherMiddleware');
|
|
||||||
const { logger, logRequestInfo, ErrorLogger, authLogger } = require('../../../config/logs');
|
|
||||||
|
|
||||||
let setupData = getSetupData();
|
// Limite de taille de fichier à 10 Go
|
||||||
let userData = getUserData();
|
const MAX_FILE_SIZE = 10 * 1024 * 1024 * 1024; // 10 Go
|
||||||
router.use(bodyParser.json());
|
|
||||||
|
|
||||||
/**
|
// Crée le dossier temporaire à la racine s'il n'existe pas
|
||||||
* @swagger
|
const tempDir = path.join(process.cwd(), 'temp');
|
||||||
* /dpanel/upload?token={token}:
|
if (!fs.existsSync(tempDir)) {
|
||||||
* post:
|
fs.mkdirSync(tempDir, { recursive: true });
|
||||||
* security:
|
|
||||||
* - bearerAuth: []
|
|
||||||
* tags:
|
|
||||||
* - File
|
|
||||||
* summary: Upload a file
|
|
||||||
* description: This route allows you to upload a file. It requires a valid JWT token in the Authorization header.
|
|
||||||
* requestBody:
|
|
||||||
* required: true
|
|
||||||
* content:
|
|
||||||
* multipart/form-data:
|
|
||||||
* schema:
|
|
||||||
* type: object
|
|
||||||
* properties:
|
|
||||||
* file:
|
|
||||||
* type: string
|
|
||||||
* format: binary
|
|
||||||
* description: The file to upload
|
|
||||||
* expiryDate:
|
|
||||||
* type: string
|
|
||||||
* format: date-time
|
|
||||||
* description: The expiry date of the file
|
|
||||||
* password:
|
|
||||||
* type: string
|
|
||||||
* description: The password to protect the file
|
|
||||||
* parameters:
|
|
||||||
* - in: header
|
|
||||||
* name: Authorization
|
|
||||||
* required: true
|
|
||||||
* schema:
|
|
||||||
* type: string
|
|
||||||
* description: The JWT token of your account to have access
|
|
||||||
* responses:
|
|
||||||
* 200:
|
|
||||||
* description: Success
|
|
||||||
* content:
|
|
||||||
* application/json:
|
|
||||||
* schema:
|
|
||||||
* type: object
|
|
||||||
* properties:
|
|
||||||
* message:
|
|
||||||
* type: string
|
|
||||||
* 400:
|
|
||||||
* description: Bad Request
|
|
||||||
* content:
|
|
||||||
* application/json:
|
|
||||||
* schema:
|
|
||||||
* type: object
|
|
||||||
* properties:
|
|
||||||
* message:
|
|
||||||
* type: string
|
|
||||||
* 401:
|
|
||||||
* description: Unauthorized
|
|
||||||
* content:
|
|
||||||
* application/json:
|
|
||||||
* schema:
|
|
||||||
* type: object
|
|
||||||
* properties:
|
|
||||||
* message:
|
|
||||||
* type: string
|
|
||||||
* 500:
|
|
||||||
* description: Error uploading the file
|
|
||||||
* content:
|
|
||||||
* application/json:
|
|
||||||
* schema:
|
|
||||||
* type: object
|
|
||||||
* properties:
|
|
||||||
* message:
|
|
||||||
* type: string
|
|
||||||
* error:
|
|
||||||
* type: string
|
|
||||||
*/
|
|
||||||
|
|
||||||
function authenticateToken(req, res, next) {
|
|
||||||
let token = null;
|
|
||||||
const authHeader = req.headers['authorization'];
|
|
||||||
|
|
||||||
if (authHeader) {
|
|
||||||
token = authHeader.split(' ')[1];
|
|
||||||
} else if (req.query.token) {
|
|
||||||
token = req.query.token;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (token == null) {
|
router.post('/', (req, res) => {
|
||||||
if (req.user) {
|
const form = new multiparty.Form({
|
||||||
return next();
|
uploadDir: tempDir,
|
||||||
} else {
|
maxFilesSize: MAX_FILE_SIZE,
|
||||||
return res.status(401).json({ message: 'Unauthorized' });
|
});
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.readFile(path.join(__dirname, '../../../data', 'user.jso,'), 'utf8', (err, data) => {
|
form.parse(req, (err, fields, files) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error('Error reading user.jso,:', err);
|
console.error('Error parsing the file:', err);
|
||||||
return res.status(401).json({ message: 'Unauthorized' });
|
return res.status(400).send('Error during the file upload');
|
||||||
}
|
}
|
||||||
|
|
||||||
const users = JSON.parse(data);
|
if (!files.file || files.file.length === 0) {
|
||||||
|
return res.status(400).send('No file uploaded');
|
||||||
const user = users.find(u => u.token === token);
|
|
||||||
|
|
||||||
if (user) {
|
|
||||||
req.user = user;
|
|
||||||
next();
|
|
||||||
} else {
|
|
||||||
return res.status(401).json({ message: 'Unauthorized' });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const file = files.file[0];
|
||||||
|
// Modifier le chemin pour être relatif à la racine
|
||||||
|
const userDir = path.join(process.cwd(), 'cdn-files', req.user.name);
|
||||||
|
|
||||||
|
// Utiliser le nom sécurisé fourni par le client
|
||||||
|
const filename = fields.filename ? fields.filename[0] : file.originalFilename;
|
||||||
|
const filePath = path.join(userDir, filename);
|
||||||
|
|
||||||
|
// Crée le répertoire s'il n'existe pas
|
||||||
|
if (!fs.existsSync(userDir)) {
|
||||||
|
fs.mkdirSync(userDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lecture en chunks pour plus de performances
|
||||||
|
const readStream = fs.createReadStream(file.path, { highWaterMark: 1024 * 1024 });
|
||||||
|
const writeStream = fs.createWriteStream(filePath, { flags: 'a' });
|
||||||
|
|
||||||
|
readStream.pipe(writeStream);
|
||||||
|
|
||||||
|
readStream.on('end', () => {
|
||||||
|
// Supprimer le fichier temporaire
|
||||||
|
fs.unlinkSync(file.path);
|
||||||
|
|
||||||
|
// Vérifier que le nom du fichier suit bien le format attendu
|
||||||
|
const fileNamePattern = /^\d{8}_[A-Z0-9]{6}_.*$/;
|
||||||
|
if (!fileNamePattern.test(filename)) {
|
||||||
|
console.warn('Le fichier uploadé ne suit pas le format de nom sécurisé attendu:', filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).send({
|
||||||
|
message: 'File uploaded successfully.',
|
||||||
|
filename: filename
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
router.get('/', (req, res) => {
|
|
||||||
res.status(400).json({ error: 'Bad Request. The request cannot be fulfilled due to bad syntax or missing parameters.' });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
router.use(fileUpload({
|
readStream.on('error', (err) => {
|
||||||
limits: { fileSize: 15 * 1024 * 1024 * 1024 },
|
console.error('Error reading the file:', err);
|
||||||
}));
|
// Nettoyer le fichier temporaire en cas d'erreur
|
||||||
|
if (fs.existsSync(file.path)) {
|
||||||
router.post('/', authenticateToken, async (req, res) => {
|
fs.unlinkSync(file.path);
|
||||||
try {
|
|
||||||
if (!req.files || Object.keys(req.files).length === 0) {
|
|
||||||
return res.status(400).send('5410 - Download error, please try again later.');
|
|
||||||
}
|
}
|
||||||
|
res.status(500).send({ message: 'Error uploading file.' });
|
||||||
const file = req.files.file;
|
|
||||||
const userId = req.user.name;
|
|
||||||
const Id = req.user.id;
|
|
||||||
const uploadDir = path.join('cdn-files', userId);
|
|
||||||
const originalFileName = file.name;
|
|
||||||
const domain = config.domain || 'mydomain.com';
|
|
||||||
let expiryDate = req.body.expiryDate;
|
|
||||||
let password = req.body.password;
|
|
||||||
|
|
||||||
if (!fs.existsSync(uploadDir)) {
|
|
||||||
fs.mkdirSync(uploadDir, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
file.mv(path.join(uploadDir, originalFileName), async (err) => {
|
|
||||||
if (err) {
|
|
||||||
console.error(err);
|
|
||||||
return res.status(500).send({ message: 'Error downloading file.' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileExtension = path.extname(originalFileName).toLowerCase();
|
|
||||||
|
|
||||||
const bcrypt = require('bcrypt');
|
|
||||||
const saltRounds = 10;
|
|
||||||
|
|
||||||
let hashedPassword = '';
|
|
||||||
if (password) {
|
|
||||||
hashedPassword = bcrypt.hashSync(password, saltRounds);
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileInfo = {
|
|
||||||
fileName: originalFileName,
|
|
||||||
expiryDate: expiryDate || '',
|
|
||||||
password: hashedPassword,
|
|
||||||
Id: Id,
|
|
||||||
path: path.join(uploadDir, originalFileName)
|
|
||||||
};
|
|
||||||
|
|
||||||
if (expiryDate || password) {
|
|
||||||
let data = [];
|
|
||||||
if (fs.existsSync(path.join(__dirname, '../../../data', 'file_info.json'))) {
|
|
||||||
const existingData = await fs.promises.readFile(path.join(__dirname, '../../../data', 'file_info.json'), 'utf8');
|
|
||||||
data = JSON.parse(existingData);
|
|
||||||
if (!Array.isArray(data)) {
|
|
||||||
data = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
data.push(fileInfo);
|
|
||||||
await fs.promises.writeFile(path.join(__dirname, '../../../data', 'file_info.json'), JSON.stringify(data, null, 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(200).send({ message: 'Your file has been successfully uploaded.' });
|
|
||||||
});
|
});
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
writeStream.on('error', (err) => {
|
||||||
return res.status(500).send({ message: 'Error downloading file.' });
|
console.error('Error writing the file:', err);
|
||||||
|
// Nettoyer le fichier temporaire en cas d'erreur
|
||||||
|
if (fs.existsSync(file.path)) {
|
||||||
|
fs.unlinkSync(file.path);
|
||||||
}
|
}
|
||||||
|
res.status(500).send({ message: 'Error uploading file.' });
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
@@ -6,10 +6,13 @@ const fsStandard = require('fs');
|
|||||||
const mime = require('mime-types');
|
const mime = require('mime-types');
|
||||||
const { logger, ErrorLogger } = require('../config/logs');
|
const { logger, ErrorLogger } = require('../config/logs');
|
||||||
const bcrypt = require('bcrypt');
|
const bcrypt = require('bcrypt');
|
||||||
const saltRounds = 10;
|
const compression = require('compression');
|
||||||
|
const { pipeline } = require('stream/promises'); // Utilisation du pipeline moderne
|
||||||
const baseDir = 'cdn-files';
|
const baseDir = 'cdn-files';
|
||||||
|
|
||||||
|
// Middleware de compression gzip
|
||||||
|
router.use(compression());
|
||||||
|
|
||||||
async function getSamAccountNameFromUserId(userId) {
|
async function getSamAccountNameFromUserId(userId) {
|
||||||
const data = await fs.readFile(path.join(__dirname, '../data', 'user.json'), 'utf8');
|
const data = await fs.readFile(path.join(__dirname, '../data', 'user.json'), 'utf8');
|
||||||
const users = JSON.parse(data);
|
const users = JSON.parse(data);
|
||||||
@@ -60,27 +63,14 @@ router.get('/:userId/:filename', async (req, res) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const filePath = await findFileInUserDir(userId, filename);
|
const filePath = await findFileInUserDir(userId, filename);
|
||||||
|
|
||||||
if (!filePath) {
|
if (!filePath) {
|
||||||
return res.render('file-not-found');
|
return res.render('file-not-found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await fs.readFile(path.join(__dirname, '../data', 'file_info.json'), 'utf8');
|
const data = await fs.readFile(path.join(__dirname, '../data', 'file_info.json'), 'utf8');
|
||||||
let fileInfoArray;
|
const fileInfoArray = JSON.parse(data);
|
||||||
try {
|
|
||||||
fileInfoArray = JSON.parse(data);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error parsing file_info.json:', error);
|
|
||||||
return res.status(500).send('Error reading file info.');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Array.isArray(fileInfoArray)) {
|
|
||||||
console.error('fileInfoArray is not an array');
|
|
||||||
return res.status(500).send('Invalid file info format.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileInfo = fileInfoArray.find(info => info.fileName === filename && info.Id === userId);
|
const fileInfo = fileInfoArray.find(info => info.fileName === filename && info.Id === userId);
|
||||||
|
|
||||||
if (fileInfo) {
|
if (fileInfo) {
|
||||||
const expiryDate = new Date(fileInfo.expiryDate);
|
const expiryDate = new Date(fileInfo.expiryDate);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
@@ -95,18 +85,41 @@ router.get('/:userId/:filename', async (req, res) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const readStream = fsStandard.createReadStream(filePath);
|
const mimeType = mime.lookup(filePath) || 'application/octet-stream';
|
||||||
let mimeType = mime.lookup(filePath) || 'application/octet-stream';
|
const range = req.headers.range;
|
||||||
|
const stats = await fs.stat(filePath);
|
||||||
|
const fileSize = stats.size;
|
||||||
|
|
||||||
|
if (range) {
|
||||||
|
const [start, end] = range.replace(/bytes=/, '').split('-');
|
||||||
|
const chunkStart = parseInt(start, 10);
|
||||||
|
const chunkEnd = end ? parseInt(end, 10) : fileSize - 1;
|
||||||
|
|
||||||
|
if (chunkStart >= fileSize || chunkEnd >= fileSize) {
|
||||||
|
res.setHeader('Content-Range', `bytes */${fileSize}`);
|
||||||
|
return res.status(416).send('Requested Range Not Satisfiable');
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(206);
|
||||||
|
res.setHeader('Content-Range', `bytes ${chunkStart}-${chunkEnd}/${fileSize}`);
|
||||||
|
res.setHeader('Accept-Ranges', 'bytes');
|
||||||
|
res.setHeader('Content-Length', chunkEnd - chunkStart + 1);
|
||||||
res.setHeader('Content-Type', mimeType);
|
res.setHeader('Content-Type', mimeType);
|
||||||
readStream.pipe(res);
|
|
||||||
|
|
||||||
if (fileInfo) {
|
const readStream = fsStandard.createReadStream(filePath, { start: chunkStart, end: chunkEnd });
|
||||||
req.session.passwordVerified = false;
|
await pipeline(readStream, res); // Utilisation de pipeline avec await pour éviter les erreurs
|
||||||
|
} else {
|
||||||
|
res.setHeader('Content-Length', fileSize);
|
||||||
|
res.setHeader('Content-Type', mimeType);
|
||||||
|
|
||||||
|
const readStream = fsStandard.createReadStream(filePath);
|
||||||
|
await pipeline(readStream, res);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
ErrorLogger.error('Error reading file:', err);
|
ErrorLogger.error('Error handling request:', err);
|
||||||
return res.status(500).send('Error reading file.');
|
if (!res.headersSent) {
|
||||||
|
res.status(500).send('Error reading file.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -116,18 +129,7 @@ router.post('/:userId/:filename', async (req, res) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await fs.readFile(path.join(__dirname, '../data', 'file_info.json'), 'utf8');
|
const data = await fs.readFile(path.join(__dirname, '../data', 'file_info.json'), 'utf8');
|
||||||
let fileInfoArray;
|
const fileInfoArray = JSON.parse(data);
|
||||||
try {
|
|
||||||
fileInfoArray = JSON.parse(data);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error parsing file_info.json:', error);
|
|
||||||
return res.status(500).send('Error reading file info.');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Array.isArray(fileInfoArray)) {
|
|
||||||
console.error('fileInfoArray is not an array');
|
|
||||||
return res.status(500).send('Invalid file info format.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileInfo = fileInfoArray.find(info => info.fileName === filename && info.Id === userId);
|
const fileInfo = fileInfoArray.find(info => info.fileName === filename && info.Id === userId);
|
||||||
|
|
||||||
@@ -138,81 +140,26 @@ router.post('/:userId/:filename', async (req, res) => {
|
|||||||
const passwordMatch = await bcrypt.compare(enteredPassword, fileInfo.password);
|
const passwordMatch = await bcrypt.compare(enteredPassword, fileInfo.password);
|
||||||
if (passwordMatch) {
|
if (passwordMatch) {
|
||||||
req.session.passwordVerified = true;
|
req.session.passwordVerified = true;
|
||||||
|
|
||||||
const filePath = await findFileInUserDir(userId, filename);
|
const filePath = await findFileInUserDir(userId, filename);
|
||||||
|
const mimeType = mime.lookup(filePath) || 'application/octet-stream';
|
||||||
const readStream = fsStandard.createReadStream(filePath);
|
const readStream = fsStandard.createReadStream(filePath);
|
||||||
let mimeType = mime.lookup(filePath) || 'application/octet-stream';
|
|
||||||
|
|
||||||
let fileContent = '';
|
let fileContent = '';
|
||||||
readStream.on('data', chunk => {
|
for await (const chunk of readStream) {
|
||||||
fileContent += chunk.toString('base64');
|
fileContent += chunk.toString('base64');
|
||||||
});
|
}
|
||||||
|
|
||||||
readStream.on('end', () => {
|
|
||||||
res.json({ success: true, fileContent, mimeType });
|
res.json({ success: true, fileContent, mimeType });
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
res.json({ success: false, message: 'Incorrect password' });
|
res.json({ success: false, message: 'Incorrect password' });
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
ErrorLogger.error('Error reading file:', err);
|
ErrorLogger.error('Error reading file:', err);
|
||||||
return res.status(500).send('Error reading file.');
|
if (!res.headersSent) {
|
||||||
|
res.status(500).send('Error reading file.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
async function deleteExpiredFiles() {
|
|
||||||
let data;
|
|
||||||
try {
|
|
||||||
data = await fs.readFile(path.join(__dirname, '../data', 'file_info.json'), 'utf8');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error reading file_info.json:', error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let fileInfoArray;
|
|
||||||
try {
|
|
||||||
fileInfoArray = JSON.parse(data);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error parsing file_info.json:', error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Array.isArray(fileInfoArray)) {
|
|
||||||
console.error('fileInfoArray is not an array');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const now = new Date();
|
|
||||||
let newFileInfoArray = [];
|
|
||||||
|
|
||||||
for (const fileInfo of fileInfoArray) {
|
|
||||||
let expiryDate;
|
|
||||||
if (fileInfo.expiryDate && fileInfo.expiryDate.trim() !== '') {
|
|
||||||
expiryDate = new Date(fileInfo.expiryDate);
|
|
||||||
} else {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (expiryDate < now) {
|
|
||||||
try {
|
|
||||||
const samaccountname = await getSamAccountNameFromUserId(fileInfo.userId);
|
|
||||||
const userDir = path.join(baseDir, samaccountname);
|
|
||||||
const filePath = path.join(userDir, fileInfo.fileName);
|
|
||||||
await fs.unlink(filePath);
|
|
||||||
} catch (err) {
|
|
||||||
ErrorLogger.error('Error deleting file:', err);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
newFileInfoArray.push(fileInfo);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await fs.writeFile(path.join(__dirname, '../data', 'file_info.json'), JSON.stringify(newFileInfoArray, null, 2), 'utf8');
|
|
||||||
} catch (err) {
|
|
||||||
ErrorLogger.error('Error writing to file_info.json:', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setInterval(deleteExpiredFiles, 24 * 60 * 60 * 1000);
|
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ const chalk = require('chalk');
|
|||||||
require('dotenv').config();
|
require('dotenv').config();
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
|
app.set('trust proxy', 1);
|
||||||
|
|
||||||
require('./models/fileCreated.js');
|
require('./models/fileCreated.js');
|
||||||
|
|
||||||
let setup;
|
let setup;
|
||||||
@@ -95,7 +97,6 @@ cron.schedule('0 * * * *', async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await fs.promises.writeFile(path.join(__dirname, 'file_info.json'), JSON.stringify(fileInfo, null, 2), 'utf8');
|
|
||||||
logger.info('Successfully checked file expirations and updated file_info.json');
|
logger.info('Successfully checked file expirations and updated file_info.json');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
ErrorLogger.error(`Failed to check file expirations: ${err}`);
|
ErrorLogger.error(`Failed to check file expirations: ${err}`);
|
||||||
|
|||||||
266
views/doc_cdn-app_api.ejs
Normal file
266
views/doc_cdn-app_api.ejs
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Documentation API CDN-APP</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background-color: #0F0F0F;
|
||||||
|
color: #F2F2F2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 40px;
|
||||||
|
background-color: #1A1A1A;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3 {
|
||||||
|
color: #4D9EFF;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.endpoint {
|
||||||
|
margin-top: 40px;
|
||||||
|
padding: 30px;
|
||||||
|
background-color: #2C2C2C;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.endpoint-title {
|
||||||
|
font-size: 1.6em;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parameters, .responses {
|
||||||
|
margin-left: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parameters ul, .responses ul {
|
||||||
|
list-style-type: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parameters li, .responses li {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parameters li:before, .responses li:before {
|
||||||
|
content: "\2022";
|
||||||
|
color: #4D9EFF;
|
||||||
|
display: inline-block;
|
||||||
|
width: 1em;
|
||||||
|
margin-left: -1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-info {
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #8C8C8C;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<h1>Documentation API CDN-APP</h1>
|
||||||
|
<div class="version-info">
|
||||||
|
<p><strong>URL de base</strong> : /api/dpanel/</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>Présentation</h2>
|
||||||
|
<p>Cette documentation décrit l'API CDN-APP, qui permet aux utilisateurs de gérer les fichiers et les dossiers au sein de l'application CDN. Elle comprend des points de terminaison pour la création, la suppression, le déplacement et la récupération de fichiers et de dossiers. Toutes les requêtes nécessitent un jeton JWT valide dans l'en-tête d'autorisation.</p>
|
||||||
|
|
||||||
|
<h2>Points de terminaison</h2>
|
||||||
|
|
||||||
|
<h3>Dossier</h3>
|
||||||
|
|
||||||
|
<div class="endpoint">
|
||||||
|
<div class="endpoint-title">1. Obtenir les fichiers et dossiers d'un dossier spécifique</div>
|
||||||
|
<p><strong>Point de terminaison</strong> : POST /dashboard/getfilefolder/{folderName}?token={token}</p>
|
||||||
|
<p><strong>Description</strong> : Cette route vous permet d'obtenir les fichiers et les dossiers d'un dossier spécifique. Elle nécessite un jeton JWT valide dans l'en-tête d'autorisation.</p>
|
||||||
|
<div class="parameters">
|
||||||
|
<p><strong>Paramètres</strong> :</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>folderName</strong> (chemin) : Le nom du dossier</li>
|
||||||
|
<li><strong>Authorization</strong> (en-tête) : Le jeton JWT de votre compte pour avoir accès</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="responses">
|
||||||
|
<p><strong>Réponses</strong> :</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>200</strong> : Succès</li>
|
||||||
|
<li><strong>401</strong> : Non autorisé</li>
|
||||||
|
<li><strong>404</strong> : Le dossier spécifié n'existe pas</li>
|
||||||
|
<li><strong>500</strong> : Erreur interne du serveur</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="endpoint">
|
||||||
|
<div class="endpoint-title">2. Supprimer un dossier spécifique</div>
|
||||||
|
<p><strong>Point de terminaison</strong> : POST /dashboard/deletefolder/{folderName}?token={token}</p>
|
||||||
|
<p><strong>Description</strong> : Cette route vous permet de supprimer un dossier spécifique. Elle nécessite un jeton JWT valide dans l'en-tête d'autorisation.</p>
|
||||||
|
<div class="parameters">
|
||||||
|
<p><strong>Paramètres</strong> :</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>folderName</strong> (chemin) : Le nom du dossier</li>
|
||||||
|
<li><strong>Authorization</strong> (en-tête) : Le jeton JWT de votre compte pour avoir accès</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="responses">
|
||||||
|
<p><strong>Réponses</strong> :</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>200</strong> : Le dossier a été supprimé avec succès</li>
|
||||||
|
<li><strong>400</strong> : Mauvaise requête</li>
|
||||||
|
<li><strong>401</strong> : Non autorisé</li>
|
||||||
|
<li><strong>403</strong> : Vous n'avez pas la permission de supprimer ce dossier</li>
|
||||||
|
<li><strong>404</strong> : Le dossier spécifié n'existe pas</li>
|
||||||
|
<li><strong>500</strong> : Erreur lors de la suppression du dossier</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="endpoint">
|
||||||
|
<div class="endpoint-title">3. Créer un nouveau dossier</div>
|
||||||
|
<p><strong>Point de terminaison</strong> : POST /dashboard/newfolder?token={token}</p>
|
||||||
|
<p><strong>Description</strong> : Cette route vous permet de créer un nouveau dossier. Elle nécessite un jeton JWT valide dans l'en-tête d'autorisation.</p>
|
||||||
|
<div class="parameters">
|
||||||
|
<p><strong>Paramètres</strong> :</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Authorization</strong> (en-tête) : Le jeton JWT de votre compte pour avoir accès</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="responses">
|
||||||
|
<p><strong>Réponses</strong> :</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>200</strong> : Succès</li>
|
||||||
|
<li><strong>400</strong> : Mauvaise requête</li>
|
||||||
|
<li><strong>401</strong> : Non autorisé</li>
|
||||||
|
<li><strong>500</strong> : Erreur lors de la création du dossier</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Fichier</h3>
|
||||||
|
|
||||||
|
<div class="endpoint">
|
||||||
|
<div class="endpoint-title">1. Supprimer un fichier spécifique</div>
|
||||||
|
<p><strong>Point de terminaison</strong> : POST /dashboard/deletefile?token={token}</p>
|
||||||
|
<p><strong>Description</strong> : Cette route vous permet de supprimer un fichier spécifique. Elle nécessite un jeton JWT valide dans l'en-tête d'autorisation.</p>
|
||||||
|
<div class="parameters">
|
||||||
|
<p><strong>Paramètres</strong> :</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Authorization</strong> (en-tête) : Le jeton JWT de votre compte pour avoir accès</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="responses">
|
||||||
|
<p><strong>Réponses</strong> :</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>200</strong> : Succès</li>
|
||||||
|
<li><strong>400</strong> : Mauvaise requête</li>
|
||||||
|
<li><strong>401</strong> : Non autorisé</li>
|
||||||
|
<li><strong>404</strong> : Le fichier spécifié n'existe pas</li>
|
||||||
|
<li><strong>500</strong> : Erreur interne du serveur</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="endpoint">
|
||||||
|
<div class="endpoint-title">2. Obtenir les informations d'un fichier</div>
|
||||||
|
<p><strong>Point de terminaison</strong> : POST /dashboard/getfile?token={token}</p>
|
||||||
|
<p><strong>Description</strong> : Cette route vous permet d'obtenir les informations sur un fichier spécifique. Elle nécessite un jeton JWT valide dans l'en-tête d'autorisation.</p>
|
||||||
|
<div class="parameters">
|
||||||
|
<p><strong>Paramètres</strong> :</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Authorization</strong> (en-tête) : Le jeton JWT de votre compte pour avoir accès</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="responses">
|
||||||
|
<p><strong>Réponses</strong> :</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>200</strong> : Succès</li>
|
||||||
|
<li><strong>400</strong> : Mauvaise requête</li>
|
||||||
|
<li><strong>401</strong> : Non autorisé</li>
|
||||||
|
<li><strong>404</strong> : Le fichier spécifié n'existe pas ou aucune information n'a été trouvée pour le fichier</li>
|
||||||
|
<li><strong>500</strong> : Erreur lors de la lecture du fichier</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="endpoint">
|
||||||
|
<div class="endpoint-title">3. Déplacer un fichier vers un dossier différent</div>
|
||||||
|
<p><strong>Point de terminaison</strong> : POST /dashboard/movefile?token={token}</p>
|
||||||
|
<p><strong>Description</strong> : Cette route vous permet de déplacer un fichier vers un dossier différent. Elle nécessite un jeton JWT valide dans l'en-tête d'autorisation.</p>
|
||||||
|
<div class="parameters">
|
||||||
|
<p><strong>Paramètres</strong> :</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Authorization</strong> (en-tête) : Le jeton JWT de votre compte pour avoir accès</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="responses">
|
||||||
|
<p><strong>Réponses</strong> :</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>200</strong> : Succès</li>
|
||||||
|
<li><strong>400</strong> : Mauvaise requête</li>
|
||||||
|
<li><strong>401</strong> : Non autorisé</li>
|
||||||
|
<li><strong>403</strong> : Tentative non autorisée d'accès à un répertoire</li>
|
||||||
|
<li><strong>500</strong> : Erreur lors du déplacement du fichier</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="endpoint">
|
||||||
|
<div class="endpoint-title">4. Renommer un fichier</div>
|
||||||
|
<p><strong>Point de terminaison</strong> : POST /dashboard/rename?token={token}</p>
|
||||||
|
<p><strong>Description</strong> : Cette route vous permet de renommer un fichier. Elle nécessite un jeton JWT valide dans l'en-tête d'autorisation.</p>
|
||||||
|
<div class="parameters">
|
||||||
|
<p><strong>Paramètres</strong> :</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Authorization</strong> (en-tête) : Le jeton JWT de votre compte pour avoir accès</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="responses">
|
||||||
|
<p><strong>Réponses</strong> :</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>200</strong> : Succès</li>
|
||||||
|
<li><strong>400</strong> : Mauvaise requête</li>
|
||||||
|
<li><strong>401</strong> : Non autorisé</li>
|
||||||
|
<li><strong>500</strong> : Erreur lors du renommage du fichier</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="endpoint">
|
||||||
|
<div class="endpoint-title">5. Télécharger un fichier</div>
|
||||||
|
<p><strong>Point de terminaison</strong> : POST /dpanel/upload?token={token}</p>
|
||||||
|
<p><strong>Description</strong> : Cette route vous permet de télécharger un fichier. Elle nécessite un jeton JWT valide dans l'en-tête d'autorisation.</p>
|
||||||
|
<div class="parameters">
|
||||||
|
<p><strong>Paramètres</strong> :</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Authorization</strong> (en-tête) : Le jeton JWT de votre compte pour avoir accès</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="responses">
|
||||||
|
<p><strong>Réponses</strong> :</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>200</strong> : Succès</li>
|
||||||
|
<li><strong>400</strong> : Mauvaise requête</li>
|
||||||
|
<li><strong>401</strong> : Non autorisé</li>
|
||||||
|
<li><strong>500</strong> : Erreur lors du téléchargement du fichier</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,33 +1,38 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html lang="fr">
|
||||||
<head>
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Fichier sécurisé</title>
|
<title>Fichier sécurisé</title>
|
||||||
<link rel="icon" href="/public/assets/homelab_logo.png" />
|
<link rel="icon" href="/public/assets/homelab_logo.png" />
|
||||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@10"></script>
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
||||||
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
|
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
|
||||||
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.custom-btn {
|
:root {
|
||||||
transition: transform 0.3s ease, background-color 0.3s ease, border-color 0.3s ease;
|
--background: 0 0% 100%;
|
||||||
color: #007BFF;
|
--foreground: 222.2 84% 4.9%;
|
||||||
background-color: transparent;
|
--card: 0 0% 100%;
|
||||||
padding: 10px 20px;
|
--primary: 222.2 47.4% 11.2%;
|
||||||
text-decoration: none;
|
--primary-foreground: 210 40% 98%;
|
||||||
display: inline-block;
|
--border: 214.3 31.8% 91.4%;
|
||||||
font-size: 16px;
|
--radius: 0.5rem;
|
||||||
margin: 4px 2px;
|
|
||||||
border-radius: 50px;
|
|
||||||
cursor: pointer;
|
|
||||||
box-shadow: 0 2px 5px rgba(0,0,0,0.25);
|
|
||||||
border: 2px solid #007BFF;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-btn:hover {
|
.dark {
|
||||||
transform: scale(1.15);
|
--background: 222.2 84% 4.9%;
|
||||||
background-color: #007BFF;
|
--foreground: 210 40% 98%;
|
||||||
color: #fff;
|
--card: 222.2 84% 4.9%;
|
||||||
|
--primary: 210 40% 98%;
|
||||||
|
--primary-foreground: 222.2 47.4% 11.2%;
|
||||||
|
--border: 217.2 32.6% 17.5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: hsl(var(--background));
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
transition: background-color 0.3s ease, color 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.animate {
|
.animate {
|
||||||
@@ -43,79 +48,298 @@
|
|||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
|
||||||
|
|
||||||
<body class="bg-gray-900 text-white">
|
.form-container {
|
||||||
<div class="flex justify-center items-center h-screen animate">
|
background-color: hsl(var(--card));
|
||||||
<div class="max-w-md mx-auto bg-gray-800 p-8 rounded shadow-md">
|
border: 1px solid hsl(var(--border));
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border: 1px solid hsl(var(--border));
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background-color: hsl(var(--background));
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background-color: hsl(var(--primary));
|
||||||
|
color: hsl(var(--primary-foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes successShake {
|
||||||
|
0%, 100% { transform: translateX(0); background-color: #10B981; }
|
||||||
|
25% { transform: translateX(-2px); }
|
||||||
|
75% { transform: translateX(2px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes failShake {
|
||||||
|
0%, 100% { transform: translateX(0); background-color: #EF4444; }
|
||||||
|
20%, 60% { transform: translateX(-4px); }
|
||||||
|
40%, 80% { transform: translateX(4px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-animation {
|
||||||
|
animation: successShake 0.5s ease;
|
||||||
|
background-color: #10B981 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fail-animation {
|
||||||
|
animation: failShake 0.5s ease;
|
||||||
|
background-color: #EF4444 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes loading-bar {
|
||||||
|
0% { transform: translateX(-100%); }
|
||||||
|
100% { transform: translateX(100%); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-loading-bar {
|
||||||
|
animation: loading-bar 1.5s infinite ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ripple {
|
||||||
|
from {
|
||||||
|
transform: translate(-50%, -50%) scale(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translate(-50%, -50%) scale(4);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="animate">
|
||||||
|
<div id="app" class="min-h-screen flex items-center justify-center">
|
||||||
|
<div class="container mt-8">
|
||||||
|
<div class="max-w-md mx-auto">
|
||||||
<h1 class="text-3xl font-bold text-center mb-8">Entrer le mot de passe pour <%= filename %></h1>
|
<h1 class="text-3xl font-bold text-center mb-8">Entrer le mot de passe pour <%= filename %></h1>
|
||||||
<p class="text-center text-red-500 mb-4">Le fichier <span class="text-blue-500"><%= filename %></span> est protégé. Veuillez entrer le mot de passe pour y accéder.</p>
|
<div class="form-container">
|
||||||
|
<p class="text-center mb-4">Le fichier <span class="text-blue-500"><%= filename %></span> est protégé. Veuillez entrer le mot de passe pour y accéder.</p>
|
||||||
<form id="password-form" action="/attachments/<%= userId %>/<%= filename %>" method="post">
|
<form id="password-form" action="/attachments/<%= userId %>/<%= filename %>" method="post">
|
||||||
<div class="mb-4">
|
<div class="form-group mb-4">
|
||||||
<label for="password" class="block text-gray-200 font-bold mb-2">Mot de passe :</label>
|
<label for="password" class="block font-bold mb-2">Mot de passe :</label>
|
||||||
<input type="password" id="password" name="password" required class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-200 bg-gray-700 leading-tight focus:outline-none focus:shadow-outline">
|
<input type="password" id="password" name="password" required class="form-control">
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<input type="submit" value="Soumettre" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline custom-btn">
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="fa-solid fa-lock mr-2"></i>
|
||||||
|
Soumettre
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="theme-switch" class="fixed top-4 right-4 btn p-2">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
$('#password-form').on('submit', function(e) {
|
// Gestion du thème
|
||||||
e.preventDefault();
|
const body = document.body;
|
||||||
|
const themeSwitcher = document.getElementById('theme-switch');
|
||||||
|
|
||||||
|
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('(prefers-color-scheme: dark)').matches) {
|
||||||
|
setTheme('dark');
|
||||||
|
}
|
||||||
|
|
||||||
|
themeSwitcher.addEventListener('click', () => {
|
||||||
|
body.classList.contains('dark') ? setTheme('light') : setTheme('dark');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Gestion des cookies
|
||||||
|
function setCookie(name, value, minutes) {
|
||||||
|
const expires = new Date(Date.now() + minutes * 60 * 1000).toUTCString();
|
||||||
|
document.cookie = `${name}=${value}; expires=${expires}; path=/`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCookie(name) {
|
||||||
|
const cookies = document.cookie.split('; ');
|
||||||
|
for (let cookie of cookies) {
|
||||||
|
const [key, value] = cookie.split('=');
|
||||||
|
if (key === name) return value;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Variables dynamiques
|
||||||
const userId = '<%= userId %>';
|
const userId = '<%= userId %>';
|
||||||
const filename = '<%= filename %>';
|
const filename = '<%= filename %>';
|
||||||
|
const cookieName = `auth_${userId}_${filename}`;
|
||||||
|
|
||||||
|
// Vérification du cookie
|
||||||
|
const savedPassword = getCookie(cookieName);
|
||||||
|
if (savedPassword) {
|
||||||
|
$.ajax({
|
||||||
|
url: `/attachments/${userId}/${filename}`,
|
||||||
|
method: 'POST',
|
||||||
|
data: { password: savedPassword }
|
||||||
|
}).done(function(response) {
|
||||||
|
if (response.success) {
|
||||||
|
window.location = `/attachments/${userId}/${filename}`;
|
||||||
|
} else {
|
||||||
|
// Si le cookie n'est plus valide, on le supprime
|
||||||
|
document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gestion du formulaire
|
||||||
|
$('#password-form').on('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
const enteredPassword = $('#password').val();
|
const enteredPassword = $('#password').val();
|
||||||
|
const submitButton = $(this).find('button[type="submit"]');
|
||||||
|
|
||||||
if (!enteredPassword) {
|
if (!enteredPassword) {
|
||||||
Swal.fire({
|
submitButton.addClass('fail-animation');
|
||||||
position: 'top',
|
submitButton.html('<i class="fas fa-times mr-2"></i>Mot de passe requis');
|
||||||
icon: 'error',
|
|
||||||
title: 'Password is required',
|
setTimeout(() => {
|
||||||
showConfirmButton: false,
|
submitButton.removeClass('fail-animation');
|
||||||
timer: 1800,
|
submitButton.prop('disabled', false);
|
||||||
toast: true
|
submitButton.html('<i class="fa-solid fa-lock mr-2"></i>Soumettre');
|
||||||
});
|
}, 1500);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$.post('/attachments/' + userId + '/' + filename, { password: enteredPassword })
|
submitButton.prop('disabled', true);
|
||||||
|
submitButton.html('<i class="fas fa-spinner fa-spin mr-2"></i>Vérification...');
|
||||||
|
|
||||||
|
$.post($(this).attr('action'), { password: enteredPassword })
|
||||||
.done(function(data) {
|
.done(function(data) {
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
Swal.fire({
|
setCookie(cookieName, enteredPassword, 15);
|
||||||
position: 'top',
|
|
||||||
icon: 'success',
|
submitButton.addClass('success-animation');
|
||||||
title: 'Mot de passe correct',
|
submitButton.html('<i class="fas fa-check mr-2"></i>Succès!');
|
||||||
text: 'Vous allez être redirigé vers le fichier !',
|
|
||||||
showConfirmButton: false,
|
const ripple = document.createElement('span');
|
||||||
timer: 1800,
|
ripple.style.cssText = `
|
||||||
toast: true
|
position: absolute;
|
||||||
}).then(() => {
|
background: rgba(255, 255, 255, 0.3);
|
||||||
window.location.href = '/attachments/' + userId + '/' + filename;
|
border-radius: 50%;
|
||||||
|
pointer-events: none;
|
||||||
|
width: 200px;
|
||||||
|
height: 200px;
|
||||||
|
transform: translate(-50%, -50%) scale(0);
|
||||||
|
animation: ripple 1s ease-out;
|
||||||
|
`;
|
||||||
|
submitButton[0].appendChild(ripple);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
const formContainer = $('.form-container');
|
||||||
|
const h1Title = $('h1');
|
||||||
|
|
||||||
|
formContainer.css({
|
||||||
|
'opacity': '0',
|
||||||
|
'transform': 'translateY(-20px) scale(0.98)',
|
||||||
|
'transition': 'all 0.3s ease'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
h1Title.css({
|
||||||
|
'transform': 'translateY(20px)',
|
||||||
|
'opacity': '0',
|
||||||
|
'transition': 'all 0.3s ease'
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
formContainer.html(`
|
||||||
|
<div class="text-center animate">
|
||||||
|
<div class="flex items-center justify-center w-20 h-20 mx-auto mb-4 bg-green-500 rounded-full">
|
||||||
|
<i class="fas fa-check text-4xl text-white"></i>
|
||||||
|
</div>
|
||||||
|
<h2 class="text-2xl font-semibold mb-3">Accès autorisé !</h2>
|
||||||
|
<p class="text-gray-500 dark:text-gray-400 mb-4">Redirection vers le fichier...</p>
|
||||||
|
<div class="w-full h-1 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
|
||||||
|
<div class="h-full bg-green-500 animate-loading-bar"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
|
||||||
|
formContainer.css({
|
||||||
|
'opacity': '1',
|
||||||
|
'transform': 'translateY(0) scale(1)'
|
||||||
|
});
|
||||||
|
|
||||||
|
h1Title.text('Succès !');
|
||||||
|
h1Title.css({
|
||||||
|
'transform': 'translateY(0)',
|
||||||
|
'opacity': '1'
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location = '/attachments/' + userId + '/' + filename;
|
||||||
|
}, 1500);
|
||||||
|
}, 300);
|
||||||
|
}, 700);
|
||||||
} else {
|
} else {
|
||||||
Swal.fire({
|
submitButton.addClass('fail-animation');
|
||||||
position: 'top',
|
submitButton.html('<i class="fas fa-times mr-2"></i>Échec');
|
||||||
icon: 'error',
|
|
||||||
title: 'Mot de passe incorrect',
|
if ('vibrate' in navigator) {
|
||||||
showConfirmButton: false,
|
navigator.vibrate(100);
|
||||||
timer: 1800,
|
}
|
||||||
toast: true
|
|
||||||
});
|
setTimeout(() => {
|
||||||
|
submitButton.removeClass('fail-animation');
|
||||||
|
submitButton.prop('disabled', false);
|
||||||
|
submitButton.html('<i class="fa-solid fa-lock mr-2"></i>Soumettre');
|
||||||
|
}, 1500);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.fail(function() {
|
.fail(function() {
|
||||||
Swal.fire({
|
submitButton.addClass('fail-animation');
|
||||||
position: 'top',
|
submitButton.html('<i class="fas fa-times mr-2"></i>Erreur');
|
||||||
icon: 'error',
|
|
||||||
title: 'Erreur lors de la vérification du mot de passe',
|
setTimeout(() => {
|
||||||
showConfirmButton: false,
|
submitButton.removeClass('fail-animation');
|
||||||
timer: 1800,
|
submitButton.prop('disabled', false);
|
||||||
toast: true
|
submitButton.html('<i class="fa-solid fa-lock mr-2"></i>Soumettre');
|
||||||
});
|
}, 1500);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>CDN - Myaxrin Labs</title>
|
<title>CDN - Myaxrin Labs</title>
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<link rel="icon" href="https://cdn.dinawo.fr/public/assets/homelab_logo.png"/>
|
||||||
<style>
|
<style>
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
||||||
|
|
||||||
|
|||||||
545
views/upload.ejs
545
views/upload.ejs
@@ -7,7 +7,6 @@
|
|||||||
<link rel="icon" href="/public/assets/homelab_logo.png" />
|
<link rel="icon" href="/public/assets/homelab_logo.png" />
|
||||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css" rel="stylesheet">
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
||||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@10"></script>
|
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
background-image: url('<%= user.wallpaper %>');
|
background-image: url('<%= user.wallpaper %>');
|
||||||
@@ -67,6 +66,57 @@
|
|||||||
--ring: 212.7 26.8% 83.9%;
|
--ring: 212.7 26.8% 83.9%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Ajout des styles pour la notification qui s'intègrent au design existant */
|
||||||
|
.notification-container {
|
||||||
|
position: fixed;
|
||||||
|
top: 1rem;
|
||||||
|
right: 1rem;
|
||||||
|
z-index: 1100;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: hsl(var(--card));
|
||||||
|
border: 1px solid hsl(var(--border));
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 1rem;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
transform: translateX(150%);
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
max-width: 350px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification.show {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-icon {
|
||||||
|
margin-right: 0.75rem;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification.success .notification-icon {
|
||||||
|
color: #10B981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification.error .notification-icon {
|
||||||
|
color: #EF4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Design original pour toutes les autres classes */
|
||||||
body {
|
body {
|
||||||
font-family: 'Inter', sans-serif;
|
font-family: 'Inter', sans-serif;
|
||||||
background-color: hsl(var(--background));
|
background-color: hsl(var(--background));
|
||||||
@@ -147,22 +197,82 @@
|
|||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Modal styles gardant le même design */
|
||||||
|
.modal {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
|
z-index: 1000;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal.show {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background: hsl(var(--card));
|
||||||
|
border: 1px solid hsl(var(--border));
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 2rem;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 2rem auto;
|
||||||
|
transform: translateY(-20px);
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal.show .modal-content {
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Styles des étapes avec le design original */
|
||||||
|
.step {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
padding: 1rem;
|
||||||
|
border: 1px solid hsl(var(--border));
|
||||||
|
border-radius: var(--radius);
|
||||||
|
opacity: 0.7;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
background-color: hsl(var(--card));
|
||||||
|
}
|
||||||
|
|
||||||
|
.step.active {
|
||||||
|
opacity: 1;
|
||||||
|
border-color: hsl(var(--primary));
|
||||||
|
}
|
||||||
|
|
||||||
.progress-bar {
|
.progress-bar {
|
||||||
height: 2px;
|
height: 4px;
|
||||||
background-color: hsl(var(--border));
|
background-color: hsl(var(--border));
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-bar div {
|
.progress-bar-fill {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: hsl(var(--primary));
|
background-color: hsl(var(--primary));
|
||||||
width: 0%;
|
width: 0;
|
||||||
|
transition: width 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.step.completed .progress-bar-fill {
|
||||||
|
background-color: #10B981;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="animate dark">
|
<body class="animate dark">
|
||||||
<button id="themeSwitcher" class="btn btn-secondary p-2">
|
<button id="themeSwitcher" class="btn btn-secondary p-2">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
||||||
@@ -170,6 +280,9 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<!-- Notification Container -->
|
||||||
|
<div class="notification-container"></div>
|
||||||
|
|
||||||
<div id="app" class="min-h-screen flex items-center justify-center">
|
<div id="app" class="min-h-screen flex items-center justify-center">
|
||||||
<div class="container mt-8">
|
<div class="container mt-8">
|
||||||
<h1 class="text-3xl font-semibold mb-6 text-center animate">Upload de Fichiers</h1>
|
<h1 class="text-3xl font-semibold mb-6 text-center animate">Upload de Fichiers</h1>
|
||||||
@@ -178,7 +291,9 @@
|
|||||||
<form id="uploadForm">
|
<form id="uploadForm">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="file" class="block mb-2">Sélectionnez un fichier :</label>
|
<label for="file" class="block mb-2">Sélectionnez un fichier :</label>
|
||||||
<input type="file" name="file" id="fileInput" accept=".zip, .pdf, .txt, .jpg, .jpeg, .png, .gif, .iso, .mp4" class="form-control">
|
<input type="file" name="file" id="fileInput"
|
||||||
|
accept=".zip, .pdf, .txt, .jpg, .jpeg, .png, .gif, .iso, .mp4"
|
||||||
|
class="form-control">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@@ -191,25 +306,14 @@
|
|||||||
<input type="password" name="password" id="password" class="form-control">
|
<input type="password" name="password" id="password" class="form-control">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group mb-4">
|
|
||||||
<label class="block mb-2">Progression :</label>
|
|
||||||
<div class="progress-bar relative">
|
|
||||||
<div id="progressBar"></div>
|
|
||||||
<div id="progressText" class="absolute inset-0 flex items-center justify-center text-white font-semibold">0%</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group mb-4">
|
|
||||||
<p id="estimatedTime" class="text-sm text-gray-400">Temps estimé : 0 min 0 sec</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit" id="uploadButton" class="btn btn-primary w-full py-2 mt-4">
|
<button type="submit" id="uploadButton" class="btn btn-primary w-full py-2 mt-4">
|
||||||
<i class="fas fa-upload icon-spacing"></i>
|
<i class="fas fa-upload icon-spacing"></i>
|
||||||
Téléverser
|
Téléverser
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<button onclick="window.location.href='/dpanel/dashboard';" class="btn btn-secondary w-full py-2 mt-4">
|
<button onclick="window.location.href='/dpanel/dashboard';"
|
||||||
|
class="btn btn-secondary w-full py-2 mt-4">
|
||||||
<i class="fas fa-arrow-left icon-spacing"></i>
|
<i class="fas fa-arrow-left icon-spacing"></i>
|
||||||
Retour au Dashboard
|
Retour au Dashboard
|
||||||
</button>
|
</button>
|
||||||
@@ -218,98 +322,353 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Upload Progress Modal -->
|
||||||
|
<div id="uploadModal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<h2 class="text-2xl font-semibold mb-6">Progression détaillée</h2>
|
||||||
|
|
||||||
|
<!-- Upload step -->
|
||||||
|
<div class="step" id="uploadStep">
|
||||||
|
<div class="step-icon">
|
||||||
|
<i class="fas fa-upload"></i>
|
||||||
|
</div>
|
||||||
|
<div class="progress-details">
|
||||||
|
<div class="flex justify-between mb-1">
|
||||||
|
<span class="font-medium">Téléchargement</span>
|
||||||
|
<span class="upload-percentage">0%</span>
|
||||||
|
</div>
|
||||||
|
<div class="progress-bar">
|
||||||
|
<div class="progress-bar-fill"></div>
|
||||||
|
</div>
|
||||||
|
<div class="eta">En attente...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Chunks step -->
|
||||||
|
<div class="step" id="chunksStep">
|
||||||
|
<div class="step-icon">
|
||||||
|
<i class="fas fa-puzzle-piece"></i>
|
||||||
|
</div>
|
||||||
|
<div class="progress-details">
|
||||||
|
<div class="flex justify-between mb-1">
|
||||||
|
<span class="font-medium">Création des chunks</span>
|
||||||
|
<span class="chunks-percentage">En attente</span>
|
||||||
|
</div>
|
||||||
|
<div class="progress-bar">
|
||||||
|
<div class="progress-bar-fill"></div>
|
||||||
|
</div>
|
||||||
|
<div class="eta">En attente...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Compilation step -->
|
||||||
|
<div class="step" id="compilationStep">
|
||||||
|
<div class="step-icon">
|
||||||
|
<i class="fas fa-cogs"></i>
|
||||||
|
</div>
|
||||||
|
<div class="progress-details">
|
||||||
|
<div class="flex justify-between mb-1">
|
||||||
|
<span class="font-medium">Compilation finale</span>
|
||||||
|
<span class="compilation-percentage">En attente</span>
|
||||||
|
</div>
|
||||||
|
<div class="progress-bar">
|
||||||
|
<div class="progress-bar-fill"></div>
|
||||||
|
</div>
|
||||||
|
<div class="eta">En attente...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
// Constants and global variables
|
||||||
|
const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB chunks
|
||||||
|
const modal = document.getElementById('uploadModal');
|
||||||
const uploadForm = document.getElementById('uploadForm');
|
const uploadForm = document.getElementById('uploadForm');
|
||||||
const fileInput = document.getElementById('fileInput');
|
const fileInput = document.getElementById('fileInput');
|
||||||
const progressBar = document.getElementById('progressBar');
|
|
||||||
const progressText = document.getElementById('progressText');
|
|
||||||
const estimatedTime = document.getElementById('estimatedTime');
|
|
||||||
|
|
||||||
|
// Theme management
|
||||||
|
const themeSwitcher = document.getElementById('themeSwitcher');
|
||||||
|
const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)');
|
||||||
|
|
||||||
|
function setTheme(theme) {
|
||||||
|
document.documentElement.setAttribute('data-theme', theme);
|
||||||
|
localStorage.setItem('theme', theme);
|
||||||
|
|
||||||
|
if (theme === 'dark') {
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
document.documentElement.classList.remove('light');
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.add('light');
|
||||||
|
document.documentElement.classList.remove('dark');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize theme
|
||||||
|
const savedTheme = localStorage.getItem('theme') || (prefersDarkScheme.matches ? 'dark' : 'light');
|
||||||
|
setTheme(savedTheme);
|
||||||
|
|
||||||
|
themeSwitcher.addEventListener('click', () => {
|
||||||
|
const currentTheme = document.documentElement.getAttribute('data-theme');
|
||||||
|
setTheme(currentTheme === 'dark' ? 'light' : 'dark');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Utility functions for file handling
|
||||||
|
async function generateSecurityCode() {
|
||||||
|
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||||
|
let code = '';
|
||||||
|
for (let i = 0; i < 6; i++) {
|
||||||
|
code += characters.charAt(Math.floor(Math.random() * characters.length));
|
||||||
|
}
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function formatSecureFileName(originalFileName) {
|
||||||
|
// Format: YYYYMMDD_SECURITY_originalname.ext
|
||||||
|
const now = new Date();
|
||||||
|
const date = now.toISOString().slice(0,10).replace(/-/g, '');
|
||||||
|
const securityCode = await generateSecurityCode();
|
||||||
|
|
||||||
|
const lastDot = originalFileName.lastIndexOf('.');
|
||||||
|
const fileName = lastDot !== -1 ? originalFileName.substring(0, lastDot) : originalFileName;
|
||||||
|
const fileExt = lastDot !== -1 ? originalFileName.substring(lastDot) : '';
|
||||||
|
|
||||||
|
return `${date}_${securityCode}_${fileName}${fileExt}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notification system
|
||||||
|
function showNotification(type, title, message = '') {
|
||||||
|
const container = document.querySelector('.notification-container');
|
||||||
|
const notification = document.createElement('div');
|
||||||
|
notification.className = `notification ${type}`;
|
||||||
|
|
||||||
|
notification.innerHTML = `
|
||||||
|
<div class="notification-icon">
|
||||||
|
<i class="fas ${type === 'success' ? 'fa-check-circle' : 'fa-times-circle'}"></i>
|
||||||
|
</div>
|
||||||
|
<div class="notification-content">
|
||||||
|
<div class="notification-title">${title}</div>
|
||||||
|
${message ? `<div class="notification-message">${message}</div>` : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Auto-remove previous notifications if more than 3
|
||||||
|
const notifications = container.querySelectorAll('.notification');
|
||||||
|
if (notifications.length >= 3) {
|
||||||
|
notifications[0].remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
container.appendChild(notification);
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
notification.classList.add('show');
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.classList.remove('show');
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.remove();
|
||||||
|
}, 300);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Progress handling
|
||||||
|
function updateProgress(step, progress, eta = '') {
|
||||||
|
const progressBar = step.querySelector('.progress-bar-fill');
|
||||||
|
const percentage = step.querySelector('[class$="-percentage"]');
|
||||||
|
const etaElement = step.querySelector('.eta');
|
||||||
|
|
||||||
|
progressBar.style.width = `${progress}%`;
|
||||||
|
percentage.textContent = `${Math.round(progress)}%`;
|
||||||
|
if (eta) etaElement.textContent = eta;
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateETA(loaded, total, startTime) {
|
||||||
|
const elapsed = (Date.now() - startTime) / 1000;
|
||||||
|
const rate = loaded / elapsed;
|
||||||
|
const remaining = (total - loaded) / rate;
|
||||||
|
const minutes = Math.floor(remaining / 60);
|
||||||
|
const seconds = Math.round(remaining % 60);
|
||||||
|
return `Temps restant estimé: ${minutes}min ${seconds}s`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modal management
|
||||||
|
function showModal() {
|
||||||
|
modal.style.display = 'block';
|
||||||
|
setTimeout(() => {
|
||||||
|
modal.classList.add('show');
|
||||||
|
modal.querySelector('.modal-content').style.opacity = '1';
|
||||||
|
}, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideModal() {
|
||||||
|
modal.classList.remove('show');
|
||||||
|
setTimeout(() => {
|
||||||
|
modal.style.display = 'none';
|
||||||
|
resetSteps();
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetSteps() {
|
||||||
|
document.querySelectorAll('.step').forEach(step => {
|
||||||
|
step.classList.remove('active', 'completed');
|
||||||
|
const progressBar = step.querySelector('.progress-bar-fill');
|
||||||
|
const percentage = step.querySelector('[class$="-percentage"]');
|
||||||
|
const eta = step.querySelector('.eta');
|
||||||
|
|
||||||
|
progressBar.style.width = '0%';
|
||||||
|
if (percentage) percentage.textContent = 'En attente';
|
||||||
|
if (eta) eta.textContent = 'En attente...';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main upload function
|
||||||
|
async function uploadFile(file) {
|
||||||
|
const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
|
||||||
|
let uploadedChunks = 0;
|
||||||
|
let uploadedBytes = 0;
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
// Générer le nom de fichier sécurisé
|
||||||
|
const secureFileName = await formatSecureFileName(file.name);
|
||||||
|
|
||||||
|
showModal();
|
||||||
|
|
||||||
|
const uploadStep = document.getElementById('uploadStep');
|
||||||
|
const chunksStep = document.getElementById('chunksStep');
|
||||||
|
const compilationStep = document.getElementById('compilationStep');
|
||||||
|
|
||||||
|
try {
|
||||||
|
uploadStep.classList.add('active');
|
||||||
|
|
||||||
|
for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) {
|
||||||
|
const start = chunkIndex * CHUNK_SIZE;
|
||||||
|
const end = Math.min(start + CHUNK_SIZE, file.size);
|
||||||
|
const chunk = file.slice(start, end);
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', chunk);
|
||||||
|
formData.append('chunkIndex', chunkIndex);
|
||||||
|
formData.append('totalChunks', totalChunks);
|
||||||
|
formData.append('filename', secureFileName);
|
||||||
|
formData.append('originalFilename', file.name);
|
||||||
|
|
||||||
|
// Ajoute la date d'expiration et le mot de passe seulement s'ils sont renseignés
|
||||||
|
const expiryDate = document.getElementById('expiryDate').value;
|
||||||
|
const password = document.getElementById('password').value;
|
||||||
|
|
||||||
|
if (expiryDate) {
|
||||||
|
formData.append('expiryDate', expiryDate);
|
||||||
|
}
|
||||||
|
if (password) {
|
||||||
|
formData.append('password', password);
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadedBytes += chunk.size;
|
||||||
|
const uploadProgress = (uploadedBytes / file.size) * 100;
|
||||||
|
|
||||||
|
updateProgress(uploadStep, uploadProgress,
|
||||||
|
calculateETA(uploadedBytes, file.size, startTime));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/dpanel/upload', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Upload failed: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadedChunks++;
|
||||||
|
|
||||||
|
// Mise à jour de la progression des chunks
|
||||||
|
const chunksProgress = (uploadedChunks / totalChunks) * 100;
|
||||||
|
updateProgress(chunksStep, chunksProgress,
|
||||||
|
`Traitement des chunks: ${uploadedChunks}/${totalChunks}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Chunk upload failed:', error);
|
||||||
|
throw new Error(`Échec de l'upload du chunk ${chunkIndex + 1}/${totalChunks}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload terminé, passer à la compilation
|
||||||
|
uploadStep.classList.remove('active');
|
||||||
|
uploadStep.classList.add('completed');
|
||||||
|
chunksStep.classList.add('completed');
|
||||||
|
compilationStep.classList.add('active');
|
||||||
|
|
||||||
|
// Simulation de la compilation (peut être remplacé par un vrai appel API)
|
||||||
|
let compilationProgress = 0;
|
||||||
|
const compilationInterval = setInterval(() => {
|
||||||
|
compilationProgress += 5;
|
||||||
|
updateProgress(compilationStep, compilationProgress,
|
||||||
|
'Assemblage du fichier final...');
|
||||||
|
|
||||||
|
if (compilationProgress >= 100) {
|
||||||
|
clearInterval(compilationInterval);
|
||||||
|
compilationStep.classList.remove('active');
|
||||||
|
compilationStep.classList.add('completed');
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
showNotification('success', 'Fichier téléchargé avec succès !',
|
||||||
|
`Nom sécurisé : ${secureFileName}`);
|
||||||
|
hideModal();
|
||||||
|
uploadForm.reset();
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Upload failed:', error);
|
||||||
|
hideModal();
|
||||||
|
showNotification('error', 'Échec du téléchargement', error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event Listeners
|
||||||
uploadForm.addEventListener('submit', async function(e) {
|
uploadForm.addEventListener('submit', async function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const file = fileInput.files[0];
|
const file = fileInput.files[0];
|
||||||
if (!file) {
|
if (!file) {
|
||||||
Swal.fire({
|
showNotification('error', 'Aucun fichier sélectionné');
|
||||||
icon: 'error',
|
|
||||||
title: 'Aucun fichier sélectionné',
|
|
||||||
showConfirmButton: false,
|
|
||||||
timer: 1800,
|
|
||||||
toast: true,
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const formData = new FormData();
|
// Vérification du mot de passe uniquement s'il est renseigné
|
||||||
formData.append('file', file);
|
const password = document.getElementById('password').value;
|
||||||
formData.append('expiryDate', document.getElementById('expiryDate').value);
|
if (password && password.length < 6) {
|
||||||
formData.append('password', document.getElementById('password').value);
|
showNotification('error', 'Le mot de passe doit contenir au moins 6 caractères');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const xhr = new XMLHttpRequest();
|
await uploadFile(file);
|
||||||
xhr.open('POST', '/api/dpanel/upload', true);
|
});
|
||||||
|
|
||||||
xhr.upload.addEventListener('progress', function(e) {
|
// File Input Change Handler
|
||||||
if (e.lengthComputable) {
|
fileInput.addEventListener('change', function(e) {
|
||||||
const percentComplete = Math.round((e.loaded / e.total) * 100);
|
const file = e.target.files[0];
|
||||||
const remainingBytes = e.total - e.loaded;
|
if (file) {
|
||||||
const bytesPerSecond = e.loaded / (Date.now() - startTime);
|
// Vérification de la taille du fichier (1GB = 1024 * 1024 * 1024 bytes)
|
||||||
const remainingSeconds = Math.ceil(remainingBytes / bytesPerSecond);
|
const MAX_FILE_SIZE = 1024 * 1024 * 1024; // 1GB en bytes
|
||||||
const minutes = Math.floor(remainingSeconds / 60);
|
|
||||||
const seconds = remainingSeconds % 60;
|
|
||||||
|
|
||||||
progressBar.style.width = `${percentComplete}%`;
|
if (file.size > MAX_FILE_SIZE) {
|
||||||
progressText.textContent = `${percentComplete}%`;
|
showNotification('error', 'Fichier trop volumineux',
|
||||||
estimatedTime.textContent = `Temps estimé : ${minutes} min ${seconds} sec`;
|
'Le support des fichiers de plus de 1GB est actuellement en développement. Veuillez réessayer plus tard.');
|
||||||
|
fileInput.value = ''; // Reset l'input file
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Afficher le nom du fichier sélectionné si la taille est acceptable
|
||||||
|
const fileName = file.name;
|
||||||
|
showNotification('success', 'Fichier sélectionné', fileName);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
xhr.onload = function() {
|
// Prevent form submission on enter key
|
||||||
if (xhr.status === 200) {
|
uploadForm.addEventListener('keypress', function(e) {
|
||||||
Swal.fire({
|
if (e.key === 'Enter') {
|
||||||
icon: 'success',
|
e.preventDefault();
|
||||||
title: 'Fichier téléchargé avec succès !',
|
|
||||||
showConfirmButton: false,
|
|
||||||
timer: 1800,
|
|
||||||
toast: true,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
Swal.fire({
|
|
||||||
icon: 'error',
|
|
||||||
title: 'Échec du téléchargement',
|
|
||||||
showConfirmButton: false,
|
|
||||||
timer: 1800,
|
|
||||||
toast: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
xhr.send(formData);
|
|
||||||
const startTime = Date.now();
|
|
||||||
});
|
|
||||||
|
|
||||||
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');
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user