V1.0.0-beta.17 Update 2
All checks were successful
continuous-integration/drone Build is passing

This commit is contained in:
2024-12-15 00:49:12 +01:00
parent 243ab5f55e
commit f7658eca22
15 changed files with 1457 additions and 501 deletions

View File

@@ -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:
- latest
- v1.0.0-beta.17
- v${VERSION}
dockerfile: Dockerfile
username:
from_secret: docker_username

104
package-lock.json generated
View File

@@ -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",

View File

@@ -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

Binary file not shown.

Binary file not shown.

BIN
public/assets/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB

View File

@@ -321,17 +321,31 @@ 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 {
@@ -459,3 +473,169 @@ position: relative !important;
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%;
}
}

View File

@@ -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();

View File

@@ -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;
// 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 });
}
if (token == null) {
if (req.user) {
return next();
} else {
return res.status(401).json({ message: 'Unauthorized' });
}
}
router.post('/', (req, res) => {
const form = new multiparty.Form({
uploadDir: tempDir,
maxFilesSize: MAX_FILE_SIZE,
});
fs.readFile(path.join(__dirname, '../../../data', 'user.jso,'), 'utf8', (err, data) => {
form.parse(req, (err, fields, files) => {
if (err) {
console.error('Error reading user.jso,:', err);
return res.status(401).json({ message: 'Unauthorized' });
console.error('Error parsing the file:', err);
return res.status(400).send('Error during the file upload');
}
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' });
if (!files.file || files.file.length === 0) {
return res.status(400).send('No file uploaded');
}
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({
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.');
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);
}
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.' });
res.status(500).send({ message: 'Error uploading file.' });
});
} catch (error) {
console.error(error);
return res.status(500).send({ message: 'Error downloading file.' });
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(500).send({ message: 'Error uploading file.' });
});
});
});
module.exports = router;

View File

@@ -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;
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);
readStream.pipe(res);
if (fileInfo) {
req.session.passwordVerified = false;
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 });
});
} 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;

View File

@@ -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
View 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>

View File

@@ -1,33 +1,38 @@
<!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">
</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;
: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;
}
.custom-btn:hover {
transform: scale(1.15);
background-color: #007BFF;
color: #fff;
.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 {
@@ -43,79 +48,298 @@
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">
.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>
<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>
<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="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 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">
<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>
</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>
$('#password-form').on('submit', function(e) {
e.preventDefault();
// 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 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>

View File

@@ -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');

View File

@@ -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,98 +322,353 @@
</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>
// 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');
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) {
e.preventDefault();
const file = fileInput.files[0];
if (!file) {
Swal.fire({
icon: 'error',
title: 'Aucun fichier sélectionné',
showConfirmButton: false,
timer: 1800,
toast: true,
});
showNotification('error', 'Aucun fichier sélectionné');
return;
}
const formData = new FormData();
formData.append('file', file);
formData.append('expiryDate', document.getElementById('expiryDate').value);
formData.append('password', document.getElementById('password').value);
// 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;
}
const xhr = new XMLHttpRequest();
xhr.open('POST', '/api/dpanel/upload', true);
await uploadFile(file);
});
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;
// 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
progressBar.style.width = `${percentComplete}%`;
progressText.textContent = `${percentComplete}%`;
estimatedTime.textContent = `Temps estimé : ${minutes} min ${seconds} sec`;
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);
}
});
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');
}
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');
// Prevent form submission on enter key
uploadForm.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
}
});
</script>