This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
kind: pipeline
|
||||
name: default
|
||||
|
||||
steps:
|
||||
- name: build-node
|
||||
image: node:latest
|
||||
@@ -11,16 +10,14 @@ steps:
|
||||
from_secret: git_password
|
||||
commands:
|
||||
- npm install
|
||||
- node -v
|
||||
|
||||
|
||||
- export VERSION=$(node -e "console.log(require('./package.json').version)")
|
||||
- name: build-docker-image
|
||||
image: plugins/docker
|
||||
settings:
|
||||
repo: swiftlogiclabs/cdn-app-insider
|
||||
tags:
|
||||
tags:
|
||||
- latest
|
||||
- v1.0.0-beta.17
|
||||
- v${VERSION}
|
||||
dockerfile: Dockerfile
|
||||
username:
|
||||
from_secret: docker_username
|
||||
|
||||
104
package-lock.json
generated
104
package-lock.json
generated
@@ -17,6 +17,7 @@
|
||||
"bcrypt": "^5.1.1",
|
||||
"chalk": "^4.1.2",
|
||||
"chokidar": "^3.6.0",
|
||||
"compression": "^1.7.5",
|
||||
"connect-flash": "^0.1.1",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"debug": "^4.3.4",
|
||||
@@ -35,6 +36,7 @@
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"mime-types": "^2.1.35",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"multiparty": "^4.2.3",
|
||||
"mysql2": "^3.6.3",
|
||||
"ncp": "^2.0.0",
|
||||
"node-cron": "^3.0.3",
|
||||
@@ -1299,6 +1301,60 @@
|
||||
"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": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
@@ -3349,6 +3405,54 @@
|
||||
"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": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz",
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"bcrypt": "^5.1.1",
|
||||
"chalk": "^4.1.2",
|
||||
"chokidar": "^3.6.0",
|
||||
"compression": "^1.7.5",
|
||||
"connect-flash": "^0.1.1",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"debug": "^4.3.4",
|
||||
@@ -36,6 +37,7 @@
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"mime-types": "^2.1.35",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"multiparty": "^4.2.3",
|
||||
"mysql2": "^3.6.3",
|
||||
"ncp": "^2.0.0",
|
||||
"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,18 +321,32 @@ position: relative !important;
|
||||
.initial-loading {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: linear-gradient(135deg,
|
||||
hsla(var(--background), 0.85),
|
||||
hsla(var(--background), 0.9)
|
||||
);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
background-color: #333;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
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 {
|
||||
position: relative;
|
||||
@@ -458,4 +472,170 @@ position: relative !important;
|
||||
to {
|
||||
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() {
|
||||
try {
|
||||
await initializeLoadingScreen();
|
||||
|
||||
@@ -1,206 +1,87 @@
|
||||
const express = require('express');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const multiparty = require('multiparty');
|
||||
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();
|
||||
let userData = getUserData();
|
||||
router.use(bodyParser.json());
|
||||
// Limite de taille de fichier à 10 Go
|
||||
const MAX_FILE_SIZE = 10 * 1024 * 1024 * 1024; // 10 Go
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /dpanel/upload?token={token}:
|
||||
* post:
|
||||
* 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) {
|
||||
if (req.user) {
|
||||
return next();
|
||||
} else {
|
||||
return res.status(401).json({ message: 'Unauthorized' });
|
||||
}
|
||||
}
|
||||
|
||||
fs.readFile(path.join(__dirname, '../../../data', 'user.jso,'), 'utf8', (err, data) => {
|
||||
if (err) {
|
||||
console.error('Error reading user.jso,:', err);
|
||||
return res.status(401).json({ message: 'Unauthorized' });
|
||||
}
|
||||
|
||||
const users = JSON.parse(data);
|
||||
|
||||
const user = users.find(u => u.token === token);
|
||||
|
||||
if (user) {
|
||||
req.user = user;
|
||||
next();
|
||||
} else {
|
||||
return res.status(401).json({ message: 'Unauthorized' });
|
||||
}
|
||||
});
|
||||
// Crée le dossier temporaire à la racine s'il n'existe pas
|
||||
const tempDir = path.join(process.cwd(), 'temp');
|
||||
if (!fs.existsSync(tempDir)) {
|
||||
fs.mkdirSync(tempDir, { recursive: true });
|
||||
}
|
||||
|
||||
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({
|
||||
limits: { fileSize: 15 * 1024 * 1024 * 1024 },
|
||||
}));
|
||||
|
||||
router.post('/', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
if (!req.files || Object.keys(req.files).length === 0) {
|
||||
return res.status(400).send('5410 - Download error, please try again later.');
|
||||
router.post('/', (req, res) => {
|
||||
const form = new multiparty.Form({
|
||||
uploadDir: tempDir,
|
||||
maxFilesSize: MAX_FILE_SIZE,
|
||||
});
|
||||
|
||||
form.parse(req, (err, fields, files) => {
|
||||
if (err) {
|
||||
console.error('Error parsing the file:', err);
|
||||
return res.status(400).send('Error during the file upload');
|
||||
}
|
||||
|
||||
if (!files.file || files.file.length === 0) {
|
||||
return res.status(400).send('No file uploaded');
|
||||
}
|
||||
|
||||
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;
|
||||
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);
|
||||
|
||||
if (!fs.existsSync(uploadDir)) {
|
||||
fs.mkdirSync(uploadDir, { recursive: true });
|
||||
// Crée le répertoire s'il n'existe pas
|
||||
if (!fs.existsSync(userDir)) {
|
||||
fs.mkdirSync(userDir, { recursive: true });
|
||||
}
|
||||
|
||||
file.mv(path.join(uploadDir, originalFileName), async (err) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
return res.status(500).send({ message: 'Error downloading file.' });
|
||||
// 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
|
||||
});
|
||||
});
|
||||
|
||||
const fileExtension = path.extname(originalFileName).toLowerCase();
|
||||
|
||||
const bcrypt = require('bcrypt');
|
||||
const saltRounds = 10;
|
||||
|
||||
let hashedPassword = '';
|
||||
if (password) {
|
||||
hashedPassword = bcrypt.hashSync(password, saltRounds);
|
||||
readStream.on('error', (err) => {
|
||||
console.error('Error reading 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.' });
|
||||
});
|
||||
|
||||
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));
|
||||
writeStream.on('error', (err) => {
|
||||
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(200).send({ message: 'Your file has been successfully uploaded.' });
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return res.status(500).send({ message: 'Error downloading file.' });
|
||||
}
|
||||
res.status(500).send({ message: 'Error uploading file.' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -6,10 +6,13 @@ const fsStandard = require('fs');
|
||||
const mime = require('mime-types');
|
||||
const { logger, ErrorLogger } = require('../config/logs');
|
||||
const bcrypt = require('bcrypt');
|
||||
const saltRounds = 10;
|
||||
|
||||
const compression = require('compression');
|
||||
const { pipeline } = require('stream/promises'); // Utilisation du pipeline moderne
|
||||
const baseDir = 'cdn-files';
|
||||
|
||||
// Middleware de compression gzip
|
||||
router.use(compression());
|
||||
|
||||
async function getSamAccountNameFromUserId(userId) {
|
||||
const data = await fs.readFile(path.join(__dirname, '../data', 'user.json'), 'utf8');
|
||||
const users = JSON.parse(data);
|
||||
@@ -60,27 +63,14 @@ router.get('/:userId/:filename', async (req, res) => {
|
||||
|
||||
try {
|
||||
const filePath = await findFileInUserDir(userId, filename);
|
||||
|
||||
if (!filePath) {
|
||||
return res.render('file-not-found');
|
||||
}
|
||||
|
||||
const data = await fs.readFile(path.join(__dirname, '../data', 'file_info.json'), 'utf8');
|
||||
let fileInfoArray;
|
||||
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 fileInfoArray = JSON.parse(data);
|
||||
|
||||
const fileInfo = fileInfoArray.find(info => info.fileName === filename && info.Id === userId);
|
||||
|
||||
if (fileInfo) {
|
||||
const expiryDate = new Date(fileInfo.expiryDate);
|
||||
const now = new Date();
|
||||
@@ -95,18 +85,41 @@ router.get('/:userId/:filename', async (req, res) => {
|
||||
}
|
||||
}
|
||||
|
||||
const readStream = fsStandard.createReadStream(filePath);
|
||||
let mimeType = mime.lookup(filePath) || 'application/octet-stream';
|
||||
const mimeType = mime.lookup(filePath) || 'application/octet-stream';
|
||||
const range = req.headers.range;
|
||||
const stats = await fs.stat(filePath);
|
||||
const fileSize = stats.size;
|
||||
|
||||
res.setHeader('Content-Type', mimeType);
|
||||
readStream.pipe(res);
|
||||
if (range) {
|
||||
const [start, end] = range.replace(/bytes=/, '').split('-');
|
||||
const chunkStart = parseInt(start, 10);
|
||||
const chunkEnd = end ? parseInt(end, 10) : fileSize - 1;
|
||||
|
||||
if (fileInfo) {
|
||||
req.session.passwordVerified = false;
|
||||
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);
|
||||
|
||||
const readStream = fsStandard.createReadStream(filePath, { start: chunkStart, end: chunkEnd });
|
||||
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) {
|
||||
ErrorLogger.error('Error reading file:', err);
|
||||
return res.status(500).send('Error reading file.');
|
||||
ErrorLogger.error('Error handling request:', err);
|
||||
if (!res.headersSent) {
|
||||
res.status(500).send('Error reading file.');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -116,18 +129,7 @@ router.post('/:userId/:filename', async (req, res) => {
|
||||
|
||||
try {
|
||||
const data = await fs.readFile(path.join(__dirname, '../data', 'file_info.json'), 'utf8');
|
||||
let fileInfoArray;
|
||||
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 fileInfoArray = JSON.parse(data);
|
||||
|
||||
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);
|
||||
if (passwordMatch) {
|
||||
req.session.passwordVerified = true;
|
||||
|
||||
const filePath = await findFileInUserDir(userId, filename);
|
||||
const mimeType = mime.lookup(filePath) || 'application/octet-stream';
|
||||
const readStream = fsStandard.createReadStream(filePath);
|
||||
let mimeType = mime.lookup(filePath) || 'application/octet-stream';
|
||||
|
||||
let fileContent = '';
|
||||
readStream.on('data', chunk => {
|
||||
for await (const chunk of readStream) {
|
||||
fileContent += chunk.toString('base64');
|
||||
});
|
||||
}
|
||||
|
||||
readStream.on('end', () => {
|
||||
res.json({ success: true, fileContent, mimeType });
|
||||
});
|
||||
res.json({ success: true, fileContent, mimeType });
|
||||
} else {
|
||||
res.json({ success: false, message: 'Incorrect password' });
|
||||
}
|
||||
} catch (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;
|
||||
|
||||
@@ -17,6 +17,8 @@ const chalk = require('chalk');
|
||||
require('dotenv').config();
|
||||
const app = express();
|
||||
|
||||
app.set('trust proxy', 1);
|
||||
|
||||
require('./models/fileCreated.js');
|
||||
|
||||
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');
|
||||
} catch (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,121 +1,345 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Fichier sécurisé</title>
|
||||
<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>
|
||||
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
--card: 0 0% 100%;
|
||||
--primary: 222.2 47.4% 11.2%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 222.2 84% 4.9%;
|
||||
--foreground: 210 40% 98%;
|
||||
--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 {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
animation: slideIn 0.4s forwards;
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.form-container {
|
||||
background-color: hsl(var(--card));
|
||||
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>
|
||||
|
||||
<style>
|
||||
.custom-btn {
|
||||
transition: transform 0.3s ease, background-color 0.3s ease, border-color 0.3s ease;
|
||||
color: #007BFF;
|
||||
background-color: transparent;
|
||||
padding: 10px 20px;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
font-size: 16px;
|
||||
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 {
|
||||
transform: scale(1.15);
|
||||
background-color: #007BFF;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.animate {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
animation: slideIn 0.4s forwards;
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<body class="bg-gray-900 text-white">
|
||||
<div class="flex justify-center items-center h-screen animate">
|
||||
<div class="max-w-md mx-auto bg-gray-800 p-8 rounded shadow-md">
|
||||
<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>
|
||||
<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>
|
||||
<form id="password-form" action="/attachments/<%= userId %>/<%= filename %>" method="post">
|
||||
<div class="mb-4">
|
||||
<label for="password" class="block text-gray-200 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">
|
||||
</div>
|
||||
<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">
|
||||
</div>
|
||||
</form>
|
||||
<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">
|
||||
<div class="form-group mb-4">
|
||||
<label for="password" class="block font-bold mb-2">Mot de passe :</label>
|
||||
<input type="password" id="password" name="password" required class="form-control">
|
||||
</div>
|
||||
<div class="flex justify-center">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fa-solid fa-lock mr-2"></i>
|
||||
Soumettre
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</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>
|
||||
// Gestion du thème
|
||||
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 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 userId = '<%= userId %>';
|
||||
const filename = '<%= filename %>';
|
||||
const enteredPassword = $('#password').val();
|
||||
const submitButton = $(this).find('button[type="submit"]');
|
||||
|
||||
if (!enteredPassword) {
|
||||
Swal.fire({
|
||||
position: 'top',
|
||||
icon: 'error',
|
||||
title: 'Password is required',
|
||||
showConfirmButton: false,
|
||||
timer: 1800,
|
||||
toast: true
|
||||
});
|
||||
submitButton.addClass('fail-animation');
|
||||
submitButton.html('<i class="fas fa-times mr-2"></i>Mot de passe requis');
|
||||
|
||||
setTimeout(() => {
|
||||
submitButton.removeClass('fail-animation');
|
||||
submitButton.prop('disabled', false);
|
||||
submitButton.html('<i class="fa-solid fa-lock mr-2"></i>Soumettre');
|
||||
}, 1500);
|
||||
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) {
|
||||
if (data.success) {
|
||||
Swal.fire({
|
||||
position: 'top',
|
||||
icon: 'success',
|
||||
title: 'Mot de passe correct',
|
||||
text: 'Vous allez être redirigé vers le fichier !',
|
||||
showConfirmButton: false,
|
||||
timer: 1800,
|
||||
toast: true
|
||||
}).then(() => {
|
||||
window.location.href = '/attachments/' + userId + '/' + filename;
|
||||
});
|
||||
setCookie(cookieName, enteredPassword, 15);
|
||||
|
||||
submitButton.addClass('success-animation');
|
||||
submitButton.html('<i class="fas fa-check mr-2"></i>Succès!');
|
||||
|
||||
const ripple = document.createElement('span');
|
||||
ripple.style.cssText = `
|
||||
position: absolute;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
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 {
|
||||
Swal.fire({
|
||||
position: 'top',
|
||||
icon: 'error',
|
||||
title: 'Mot de passe incorrect',
|
||||
showConfirmButton: false,
|
||||
timer: 1800,
|
||||
toast: true
|
||||
});
|
||||
submitButton.addClass('fail-animation');
|
||||
submitButton.html('<i class="fas fa-times mr-2"></i>Échec');
|
||||
|
||||
if ('vibrate' in navigator) {
|
||||
navigator.vibrate(100);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
submitButton.removeClass('fail-animation');
|
||||
submitButton.prop('disabled', false);
|
||||
submitButton.html('<i class="fa-solid fa-lock mr-2"></i>Soumettre');
|
||||
}, 1500);
|
||||
}
|
||||
})
|
||||
.fail(function() {
|
||||
Swal.fire({
|
||||
position: 'top',
|
||||
icon: 'error',
|
||||
title: 'Erreur lors de la vérification du mot de passe',
|
||||
showConfirmButton: false,
|
||||
timer: 1800,
|
||||
toast: true
|
||||
});
|
||||
submitButton.addClass('fail-animation');
|
||||
submitButton.html('<i class="fas fa-times mr-2"></i>Erreur');
|
||||
|
||||
setTimeout(() => {
|
||||
submitButton.removeClass('fail-animation');
|
||||
submitButton.prop('disabled', false);
|
||||
submitButton.html('<i class="fa-solid fa-lock mr-2"></i>Soumettre');
|
||||
}, 1500);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>CDN - Myaxrin Labs</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="icon" href="https://cdn.dinawo.fr/public/assets/homelab_logo.png"/>
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
||||
|
||||
|
||||
569
views/upload.ejs
569
views/upload.ejs
@@ -7,7 +7,6 @@
|
||||
<link rel="icon" href="/public/assets/homelab_logo.png" />
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@10"></script>
|
||||
<style>
|
||||
body {
|
||||
background-image: url('<%= user.wallpaper %>');
|
||||
@@ -67,6 +66,57 @@
|
||||
--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 {
|
||||
font-family: 'Inter', sans-serif;
|
||||
background-color: hsl(var(--background));
|
||||
@@ -147,22 +197,82 @@
|
||||
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 {
|
||||
height: 2px;
|
||||
height: 4px;
|
||||
background-color: hsl(var(--border));
|
||||
border-radius: var(--radius);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.progress-bar div {
|
||||
.progress-bar-fill {
|
||||
height: 100%;
|
||||
background-color: hsl(var(--primary));
|
||||
width: 0%;
|
||||
width: 0;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.step.completed .progress-bar-fill {
|
||||
background-color: #10B981;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="animate dark">
|
||||
<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">
|
||||
@@ -170,6 +280,9 @@
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Notification Container -->
|
||||
<div class="notification-container"></div>
|
||||
|
||||
<div id="app" class="min-h-screen flex items-center justify-center">
|
||||
<div class="container mt-8">
|
||||
<h1 class="text-3xl font-semibold mb-6 text-center animate">Upload de Fichiers</h1>
|
||||
@@ -178,7 +291,9 @@
|
||||
<form id="uploadForm">
|
||||
<div class="form-group">
|
||||
<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 class="form-group">
|
||||
@@ -191,25 +306,14 @@
|
||||
<input type="password" name="password" id="password" class="form-control">
|
||||
</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">
|
||||
<i class="fas fa-upload icon-spacing"></i>
|
||||
Téléverser
|
||||
</button>
|
||||
</form>
|
||||
<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>
|
||||
Retour au Dashboard
|
||||
</button>
|
||||
@@ -218,100 +322,355 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const uploadForm = document.getElementById('uploadForm');
|
||||
const fileInput = document.getElementById('fileInput');
|
||||
const progressBar = document.getElementById('progressBar');
|
||||
const progressText = document.getElementById('progressText');
|
||||
const estimatedTime = document.getElementById('estimatedTime');
|
||||
<!-- 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>
|
||||
|
||||
uploadForm.addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
const file = fileInput.files[0];
|
||||
if (!file) {
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: 'Aucun fichier sélectionné',
|
||||
showConfirmButton: false,
|
||||
timer: 1800,
|
||||
toast: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
<!-- 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>
|
||||
// Constants and global variables
|
||||
const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB chunks
|
||||
const modal = document.getElementById('uploadModal');
|
||||
const uploadForm = document.getElementById('uploadForm');
|
||||
const fileInput = document.getElementById('fileInput');
|
||||
|
||||
// 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', file);
|
||||
formData.append('expiryDate', document.getElementById('expiryDate').value);
|
||||
formData.append('password', document.getElementById('password').value);
|
||||
formData.append('file', chunk);
|
||||
formData.append('chunkIndex', chunkIndex);
|
||||
formData.append('totalChunks', totalChunks);
|
||||
formData.append('filename', secureFileName);
|
||||
formData.append('originalFilename', file.name);
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', '/api/dpanel/upload', true);
|
||||
|
||||
xhr.upload.addEventListener('progress', function(e) {
|
||||
if (e.lengthComputable) {
|
||||
const percentComplete = Math.round((e.loaded / e.total) * 100);
|
||||
const remainingBytes = e.total - e.loaded;
|
||||
const bytesPerSecond = e.loaded / (Date.now() - startTime);
|
||||
const remainingSeconds = Math.ceil(remainingBytes / bytesPerSecond);
|
||||
const minutes = Math.floor(remainingSeconds / 60);
|
||||
const seconds = remainingSeconds % 60;
|
||||
|
||||
progressBar.style.width = `${percentComplete}%`;
|
||||
progressText.textContent = `${percentComplete}%`;
|
||||
estimatedTime.textContent = `Temps estimé : ${minutes} min ${seconds} sec`;
|
||||
}
|
||||
});
|
||||
|
||||
xhr.onload = function() {
|
||||
if (xhr.status === 200) {
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
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');
|
||||
// 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}`);
|
||||
}
|
||||
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');
|
||||
}
|
||||
// Upload terminé, passer à la compilation
|
||||
uploadStep.classList.remove('active');
|
||||
uploadStep.classList.add('completed');
|
||||
chunksStep.classList.add('completed');
|
||||
compilationStep.classList.add('active');
|
||||
|
||||
themeSwitcher.addEventListener('click', function() {
|
||||
if (body.classList.contains('dark')) {
|
||||
setTheme('light');
|
||||
} else {
|
||||
setTheme('dark');
|
||||
// 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) {
|
||||
e.preventDefault();
|
||||
|
||||
const file = fileInput.files[0];
|
||||
if (!file) {
|
||||
showNotification('error', 'Aucun fichier sélectionné');
|
||||
return;
|
||||
}
|
||||
|
||||
// Vérification du mot de passe uniquement s'il est renseigné
|
||||
const password = document.getElementById('password').value;
|
||||
if (password && password.length < 6) {
|
||||
showNotification('error', 'Le mot de passe doit contenir au moins 6 caractères');
|
||||
return;
|
||||
}
|
||||
|
||||
await uploadFile(file);
|
||||
});
|
||||
|
||||
// File Input Change Handler
|
||||
fileInput.addEventListener('change', function(e) {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
// Vérification de la taille du fichier (1GB = 1024 * 1024 * 1024 bytes)
|
||||
const MAX_FILE_SIZE = 1024 * 1024 * 1024; // 1GB en bytes
|
||||
|
||||
if (file.size > MAX_FILE_SIZE) {
|
||||
showNotification('error', 'Fichier trop volumineux',
|
||||
'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);
|
||||
}
|
||||
});
|
||||
|
||||
// Prevent form submission on enter key
|
||||
uploadForm.addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
Reference in New Issue
Block a user