First commit of the new Insider version on docker

This commit is contained in:
2024-03-27 18:20:08 +01:00
parent 7637b068f9
commit be57c29e6e
61 changed files with 13693 additions and 1 deletions

7
.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
/cdn-files/
/logs/
/report/
/user.json
/setup.json
/file_info.json
/node_modules/

View File

@@ -0,0 +1,54 @@
const fs = require('fs');
const path = require('path');
const { getUserData } = require('../Middlewares/watcherMiddleware');
const filePath = path.join(__dirname, '../user.json');
async function checkUserExistsAD(req, res, next) {
let userData = getUserData();
if (!req.user || (!req.user.name && !req.user.id)) {
return res.status(500).send('Internal Server Error');
}
try {
let users = userData;
let existingUser;
if (req.user.name) {
existingUser = users.find(u => u.name === req.user.name);
} else if (req.user.id) {
existingUser = users.find(u => u.id === req.user.id);
}
if (!existingUser) {
const id = Math.floor(Math.random() * 1e19);
const newUser = {
id: id.toString(),
name: req.user.name || req.user.id,
role: "user"
};
users.push(newUser);
try {
await fs.promises.writeFile(filePath, JSON.stringify(users, null, 2), 'utf8');
} catch (error) {
console.error(`Failed to write to ${filePath}: ${error}`);
return next(error);
}
req.user = newUser;
req.session.userId = newUser.id;
} else {
req.user = existingUser;
req.session.userId = existingUser.id;
res.render('AuthLogin', { isAuthenticated: true, setupData: {}, currentUrl: req.originalUrl, errorMessage: '' });
}
return next();
} catch (error) {
return next(error);
}
}
module.exports = { checkUserExistsAD };

View File

@@ -0,0 +1,42 @@
const { getUserData } = require('../Middlewares/watcherMiddleware');
let userData = getUserData();
async function checkUserExistsDiscord(req, res, next) {
if (!req.user || (!req.user.username && !req.user.id)) {
return res.status(500).send('Internal Server Error');
}
try {
let users = userData;
let existingUser;
if (req.user.username) {
existingUser = users.find(u => u.name === req.user.username);
} else if (req.user.id) {
existingUser = users.find(u => u.id === req.user.id);
}
if (existingUser) {
req.user.id = existingUser.id;
res.redirect('/dpanel/dashboard');
return;
}
const newUser = {
id: req.user.id,
name: req.user.username,
role: "user"
};
users.push(newUser);
await fs.writeFile(filePath, JSON.stringify(users, null, 2), 'utf8');
req.user.id = newUser.id;
return next();
} catch (error) {
return next(error);
}
}
module.exports = { checkUserExistsDiscord };

View File

@@ -0,0 +1,30 @@
const fs = require('fs');
const { logger, logRequestInfo, ErrorLogger, authLogger } = require('../config/logs');
const authMiddleware = async (req, res, next) => {
if (req.isAuthenticated() || (req.user && req.user.name)) {
const data = await fs.promises.readFile('user.json', 'utf8');
const users = JSON.parse(data);
const user = users.find(user => user.name === req.user.name);
if (!user) {
authLogger.info('User is not authenticated and user name is not set');
return res.redirect('/auth/login');
}
if (!req.session.user) {
authLogger.info('User connection attempt in progress, verification in progress...');
authLogger.info(`Login successfully completed, logged in user is: id=${user.id}, name=${user.name}, role=${user.role}, IP: ${req.ip}, User Agent: ${req.headers['user-agent']}`);
}
res.locals.user = user;
req.session.user = user;
req.userData = user;
return next();
} else {
authLogger.info(`Authentication failed for IP: ${req.ip}, User Agent: ${req.headers['user-agent']}. Redirecting to login.`);
res.redirect('/auth/login');
}
};
module.exports = authMiddleware;

View File

@@ -0,0 +1,34 @@
const fetch = (...args) => import('node-fetch').then(({ default: fetch }) => fetch(...args));
const semver = require('semver');
async function checkUpdates(req, res) {
try {
const { version: currentVersion } = require('../package.json');
const response = await fetch('https://apollon.dinawo.fr/api/get/version');
const data = await response.json();
if (data && data.update && data.update.version) {
const latestVersion = data.update.version;
const comparisonResult = semver.compare(currentVersion, latestVersion);
if (comparisonResult < 0) {
res.json({
updateAvailable: true,
message: 'Nouvelle mise à jour disponible. Voulez-vous mettre à jour maintenant ?',
showButton: true,
});
} else {
res.json({ updateAvailable: false });
}
} else {
res.status(500).json({ error: 'Données de mise à jour invalides.' });
}
} catch (error) {
console.error('Erreur lors de la vérification de la mise à jour :', error);
res.status(500).json({ error: 'Erreur lors de la vérification de la mise à jour.' });
}
}
module.exports = { checkUpdates };

View File

@@ -0,0 +1,27 @@
const path = require('path');
const chokidar = require('chokidar');
const fs = require('fs');
let userData = require(path.resolve(__dirname, '../user.json'));
let setupData = require(path.resolve(__dirname, '../setup.json'));
const watcher = chokidar.watch([path.resolve(__dirname, '../user.json'), path.resolve(__dirname, '../setup.json')], {
persistent: true
});
watcher.on('change', (filePath) => {
delete require.cache[require.resolve(filePath)];
if (filePath === path.resolve(__dirname, '../user.json')) {
userData = require(filePath);
} else if (filePath === path.resolve(__dirname, '../setup.json')) {
setupData = require(filePath);
}
console.log('File', filePath, 'has been changed');
});
module.exports = {
getUserData: () => userData,
getSetupData: () => setupData
};

View File

@@ -1,2 +1,51 @@
# CDN-APP-INSIDER
# CDN-APP 🚀
The modern CDN for secure file transfer
Boost your project with our self-hosted CDN
Experience exceptional performance and ultra-fast content distribution.
## Table of Contents
- [Installation](#installation)
- [Usage](#usage)
- [Contributing](#contributing)
- [License](#license)
- [Features](#features)
## Installation
### Prerequisites:
A CDN-Access group in your ldap.
Be connected as root.
1. `apt update && apt upgrade -y && apt install curl -y `
2. `curl -s https://apollon.dinawo.fr/getcdn/install/latest | bash`
3. `source ~/.bashrc`
4. Follow the instructions given at the end of the installation for CDN setup
Congratulations, you have successfully configured and installed the CDN on your system. To access the dashboard, simply go to *https://your-ip:3000/dpanel/dashboard*
## Usage
🔧 Instructions on how to use your project and any relevant examples.
## Contributing
🤝 I thank WaYy, main contributor.
## Features
- File Upload to CDN: Allows you to upload files to the CDN for secure and reliable storage.
- File Renaming and Deletion: Easily rename and delete files according to your needs.
- File Management: Facilitates efficient management and organization of your files.
- File Filtering by Extension: Sort files based on their extensions for easy searching.
- User-Friendly Interface with Dark Mode: Provides a user-friendly experience with a dark mode for extended usage.
- User Management with Multiple Authentication Systems: Facilitates user management by integrating multiple authentication systems (Discord, OpenID, ActiveDirectory, Google).

101
config/logs.js Normal file
View File

@@ -0,0 +1,101 @@
const { format } = require('winston');
const winston = require('winston');
const DailyRotateFile = require('winston-daily-rotate-file');
const logPrefix = (label) => {
if (label === 'server') {
return '[\x1b[32mServer\x1b[0m] ';
} else if (label === 'client') {
return '[\x1b[34mClient\x1b[0m] ';
} else if (label === 'Internal-Server-Error') {
return '[\x1b[38;5;9mInternal-Error\x1b[0m] ';
} else if (label === 'Authentification') {
return '[\x1b[33mAuthentification\x1b[0m] ';
}
return '';
};
const createDailyRotateFileTransport = () => {
return new DailyRotateFile({
filename: 'logs/log-%DATE%.log',
datePattern: 'YYYY-MM-DD',
zippedArchive: false,
maxSize: '20m',
maxFiles: '14d'
});
};
const logger = winston.createLogger({
format: format.combine(
format.label({ label: 'server' }),
format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
format.printf(info => {
const { timestamp, level, label, message } = info;
const prefix = logPrefix(label);
return `[${timestamp}] ${prefix}${message}`;
})
),
transports: [
new winston.transports.Console(),
createDailyRotateFileTransport()
]
});
const ErrorLogger = winston.createLogger({
format: format.combine(
format.label({ label: 'Internal-Server-Error' }),
format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
format.printf(info => {
const { timestamp, level, label, message } = info;
const prefix = logPrefix(label);
return `[${timestamp}] ${prefix}${message}`;
})
),
transports: [
new winston.transports.Console(),
createDailyRotateFileTransport()
]
});
const clientLogger = winston.createLogger({
format: format.combine(
format.label({ label: 'client' }),
format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
format.printf(info => {
const { timestamp, level, label, message } = info;
const prefix = logPrefix(label);
return `[${timestamp}] ${prefix}${message}`;
})
),
transports: [
new winston.transports.Console(),
createDailyRotateFileTransport()
]
});
const authLogger = winston.createLogger({
format: format.combine(
format.label({ label: 'Authentification' }),
format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
format.printf(info => {
const { timestamp, level, label, message } = info;
const prefix = logPrefix(label);
return `[${timestamp}] ${prefix}${message}`;
})
),
transports: [
new winston.transports.Console(),
createDailyRotateFileTransport()
]
});
const logRequestInfo = (req, res, next) => {
const ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
const userAgent = req.headers['user-agent'];
req.log = clientLogger;
req.log.info(`[${ip}] - ${userAgent} - ${req.method} ${req.url}`);
next();
};
module.exports = { logger, clientLogger, ErrorLogger, logRequestInfo, authLogger };

View File

@@ -0,0 +1,28 @@
const mysql = require('mysql2/promise');
const pool = require('../config/database');
async function getParams() {
const connection = await pool.getConnection();
try {
const [rows] = await connection.execute('SELECT * FROM cdn');
return {
users: rows[0].users,
};
} finally {
connection.release();
}
}
async function updateParams(params) {
const connection = await pool.getConnection();
try {
await connection.execute('UPDATE cdn SET ?', [params]);
} finally {
connection.release();
}
}
module.exports = {
getParams,
updateParams
};

49
files.json Normal file
View File

@@ -0,0 +1,49 @@
{
"routes": [
"/routes/index.js",
"/routes/auth.js",
"/routes/dpanel.js",
"/routes/attachments.js"
],
"controllers": [
"/controllers/paramController.js"
],
"models": [
"/models/AuthUser.js",
"/models/paramModel.js",
"/models/Passport-ActiveDirectory.js",
"/models/Passport-Discord.js",
"/models/updateHelper.js",
"/models/updateManager.js",
"/models/reportManager.js",
"/models/Usermanager.js"
],
"views": [
"/views/AuthLogin.ejs",
"/views/dashboard.ejs",
"/views/acces-denied.ejs",
"/views/error-recovery.ejs",
"/views/file-not-found.ejs",
"/views/folder.ejs",
"/views/parametres.ejs",
"/views/unauthorized.ejs",
"/views/upload.ejs"
],
"config": [
"/config/logs.js"
],
"Middleware": [
"/Middleware/authMiddleware.js",
"/Middleware/checkUpdate.js",
"/Middleware/UserIDMiddlewareAD.js",
"/Middleware/UserIDMiddlewareDiscord.js"
],
"json-file": [
"/package.json",
"/package-lock.json",
"/user.json",
"/files.json",
"/file-info.json",
"/setup.json"
]
}

29
models/AuthUser.js Normal file
View File

@@ -0,0 +1,29 @@
const pgp = require('pg-promise')
const pool = require('../config/database');
const bcrypt = require('bcrypt');
async function registerUser(username, password) {
const hashedPassword = await bcrypt.hash(password, 10);
const connection = await pool.getConnection();
try {
const [rows] = await connection.execute('INSERT INTO cdn (username, password) VALUES (?, ?)', [username, hashedPassword]);
return rows.insertId;
} finally {
connection.release();
}
}
async function getUserByUsername(username) {
const connection = await pool.getConnection();
try {
const [rows] = await connection.execute('SELECT * FROM cdn WHERE username = ?', [username]);
return rows[0];
} finally {
connection.release();
}
}
module.exports = {
registerUser,
getUserByUsername,
};

View File

@@ -0,0 +1,49 @@
const passport = require('passport');
const ActiveDirectoryStrategy = require('passport-activedirectory');
const fs = require('fs');
const path = require('path');
const { getUserData } = require('../Middlewares/watcherMiddleware');
const setupFilePath = path.join('setup.json');
const setupData = JSON.parse(fs.readFileSync(setupFilePath, 'utf-8'));
passport.use(new ActiveDirectoryStrategy({
integrated: false,
ldap: {
url: setupData.ldap.url,
baseDN: setupData.ldap.baseDN,
username: setupData.ldap.username,
password: setupData.ldap.password
}
}, function (profile, ad, done) {
ad.isUserMemberOf(profile._json.dn, 'CDN-Access', function (err, isMember) {
if (err) return done(err);
if (!isMember) {
return done(null, false, { message: 'L\'utilisateur n\'est pas autorisé.' });
}
return done(null, profile);
});
}));
passport.serializeUser((user, done) => {
done(null, user.name);
});
passport.deserializeUser((id, done) => {
const users = getUserData();
console.log('id:', id);
console.log('users:', users);
const user = users.find(u => u.name === id.name || u.name === `.${id.name}`);
if (user) {
return done(null, user);
} else {
return done(new Error('User not valid'), null);
}
});
module.exports = passport;

View File

@@ -0,0 +1,46 @@
const passport = require('passport');
const DiscordStrategy = require('passport-discord').Strategy;
const fs = require('fs');
const path = require('path');
const setupFilePath = path.join('setup.json');
const setupData = JSON.parse(fs.readFileSync(setupFilePath, 'utf-8'));
passport.use(new DiscordStrategy({
clientID: setupData.discord.clientID,
clientSecret: setupData.discord.clientSecret,
callbackURL: `http://${setupData.domain}/auth/discord/callback`
}, (accessToken, refreshToken, profile, done) => {
fs.readFile('user.json', 'utf8', (err, data) => {
if (err) {
return done(err);
}
const users = JSON.parse(data);
const user = users.find(user => user.id === profile.id);
if (setupData.discord.authorizedIDs.length > 0 && !setupData.discord.authorizedIDs.includes(profile.id)) {
return done(null, false, { message: 'L\'utilisateur n\'est pas autorisé.' });
}
done(null, user);
});
}));
passport.serializeUser((user, done) => {
done(null, user);
});
passport.deserializeUser((id, done) => {
fs.readFile('user.json', 'utf8', (err, data) => {
if (err) {
return done(err);
}
const users = JSON.parse(data);
const user = users.find(user => user.id === id);
done(null, user);
});
});
module.exports = passport;

33
models/Passport-Google.js Normal file
View File

@@ -0,0 +1,33 @@
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
const fs = require('fs');
const path = require('path');
const setupFilePath = path.join('setup.json');
const setupData = JSON.parse(fs.readFileSync(setupFilePath, 'utf-8'));
passport.use(new GoogleStrategy({
clientID: setupData.google.clientID,
clientSecret: setupData.google.clientSecret,
callbackURL: setupData.google.callbackURL
},
function(accessToken, refreshToken, profile, done) {
// Ici, vous pouvez vérifier si l'utilisateur est autorisé ou non
// Pour cet exemple, nous supposons que tous les utilisateurs Google sont autorisés
return done(null, profile);
}
));
passport.serializeUser((user, done) => {
done(null, user);
});
passport.deserializeUser((user, done) => {
if (user && (user.id)) {
return done(null, user);
} else {
return done(new Error('User not valid'), null);
}
});
module.exports = passport;

47
models/UserManagment.js Normal file
View File

@@ -0,0 +1,47 @@
const mysql = require('mysql2/promise');
const pool = require('../config/database');
const userSchema = {
username: {
type: 'VARCHAR(255)',
allowNull: false,
unique: true
},
password: {
type: 'VARCHAR(255)',
allowNull: false
},
isAdmin: {
type: 'BOOLEAN',
allowNull: false,
defaultValue: false
}
};
async function createUser(username, password, isAdmin) {
const connection = await pool.getConnection();
try {
const [rows] = await connection.execute(
'INSERT INTO cdn (username, password, isAdmin) VALUES (?, ?, ?)',
[username, password, isAdmin]
);
return rows.insertId;
} finally {
connection.release();
}
}
async function getUserByUsername(username) {
const connection = await pool.getConnection();
try {
const [rows] = await connection.execute('SELECT * FROM users WHERE username = ?', [username]);
return rows[0];
} finally {
connection.release();
}
}
module.exports = {
createUser,
getUserByUsername
};

16
models/paramModel.js Normal file
View File

@@ -0,0 +1,16 @@
const mysql = require('mysql2/promise');
const pool = require('../config/database');
async function getUsers() {
const connection = await pool.getConnection();
try {
const [rows] = await connection.execute('SELECT * FROM cdn');
return rows || [];
} finally {
connection.release();
}
}
module.exports = {
getUsers
};

100
models/reportManager.js Normal file
View File

@@ -0,0 +1,100 @@
const os = require('os');
const fs = require('fs');
const path = require('path');
const ip = require('ip');
const { logger, logRequestInfo, ErrorLogger } = require('../config/logs');
const packageJson = require('../package.json');
const si = require('systeminformation');
const fetch = require('node-fetch');
class SystemReport {
static async generate() {
const date = new Date();
date.setDate(date.getDate() - 1);
const previousDate = date.toISOString().split('T')[0];
const logFile = path.join(__dirname, '..', 'logs', `log-${previousDate}.log`);
const logs = fs.readFileSync(logFile, 'utf-8');
const internalErrors = logs.split('\n').filter(line => /\[38;5;9mInternal-Error/.test(line));
if (internalErrors.length === 0) {
logger.info('No internal errors in yesterday\'s logs. No report will be generated.');
return null;
}
function formatUptime(uptime) {
const days = Math.floor(uptime / (24 * 60 * 60));
uptime %= (24 * 60 * 60);
const hours = Math.floor(uptime / (60 * 60));
uptime %= (60 * 60);
const minutes = Math.floor(uptime / 60);
return `${days}d ${hours}h ${minutes}m`;
}
const osInfo = {
type: os.type(),
platform: os.platform(),
arch: os.arch(),
release: os.release(),
uptime: formatUptime(os.uptime()),
loadavg: os.loadavg().map(load => (load / os.cpus().length) * 100)
};
const networkInterfaces = os.networkInterfaces();
const serverUptime = os.uptime();
const systemLoad = os.loadavg();
const diskUsage = await si.fsSize();
const cpuTemperature = await si.cpuTemperature();
const userInfo = os.userInfo();
const systemInfo = {
memoryInfo: ((os.totalmem() - os.freemem()) / os.totalmem() * 100).toFixed(2),
cpuInfo: (os.cpus().length / os.cpus().length * 100).toFixed(2),
diskInfo: ((os.totalmem() - os.freemem()) / os.totalmem() * 100).toFixed(2),
ipAddress: ip.address(),
cdnVersion: packageJson.version,
osInfo: osInfo,
userInfo: userInfo,
errors: internalErrors,
networkInterfaces: networkInterfaces,
serverUptime: serverUptime,
systemLoad: systemLoad,
diskUsage: diskUsage,
cpuTemperature: cpuTemperature,
};
const filename = path.join(__dirname, '..', 'report', `report_${previousDate}_${ip.address()}.json`);
fs.writeFileSync(filename, JSON.stringify(systemInfo, null, 2));
logger.info("Preparing to send report...");
logger.info("Report:", JSON.stringify(systemInfo, null, 2));
try {
const response = await fetch('https://apollon.dinawo.fr/api/report/receive', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(systemInfo)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
} else {
const data = await response.json();
logger.info('Report sent successfully. Response data:', data);
}
} catch (error) {
console.error(error);
logger.error('Failed to send report. Error:', error);
}
return systemInfo;
}
}
module.exports = SystemReport;

20
models/updateHelper.js Normal file
View File

@@ -0,0 +1,20 @@
const fs = require('fs');
const fetch = (...args) => import('node-fetch').then(({ default: fetch }) => fetch(...args));
const AdmZip = require('adm-zip');
const { logger, ErrorLogger, logRequestInfo } = require('../config/logs');
function compareAndUpdate(currentPath, updatedPath) {
}
async function downloadUpdate(updateUrl, destinationPath) {
const response = await fetch(updateUrl);
const buffer = await response.buffer();
fs.writeFileSync(destinationPath, buffer);
}
function unzipUpdate(zipPath, extractionPath) {
const zip = new AdmZip(zipPath);
zip.extractAllTo(extractionPath, /*overwrite*/ true);
}
module.exports = { compareAndUpdate, downloadUpdate, unzipUpdate };

141
models/updateManager.js Normal file
View File

@@ -0,0 +1,141 @@
const fetch = (...args) => import('node-fetch').then(({ default: fetch }) => fetch(...args));
const fs = require('fs').promises;
const path = require('path');
const AdmZip = require('adm-zip');
const util = require('util');
const ncp = util.promisify(require('ncp').ncp);
const { downloadUpdate, unzipUpdate } = require('../models/updateHelper');
const { logger, ErrorLogger } = require('../config/logs');
async function getFilesInVersion() {
try {
const response = await fetch('https://apollon.dinawo.fr/api/get/version');
const data = await response.json();
return data.update.files || [];
} catch (error) {
throw new Error(`Error getting files in version: ${error.message}`);
}
}
async function listFilesInZip(zipFilePath) {
try {
const zip = new AdmZip(zipFilePath);
const zipEntries = zip.getEntries();
return zipEntries.map(entry => entry.entryName);
} catch (error) {
throw new Error(`Error listing files in zip: ${error.message}`);
}
}
async function deleteFileOrDirectory(filePath) {
try {
const stats = await fs.stat(filePath);
if (stats.isDirectory()) {
logger.info(`Skipped deletion of directory: ${filePath}`);
} else {
await fs.unlink(filePath);
logger.info(`Updated file deleted: ${filePath}`);
}
} catch (error) {
throw new Error(`Error deleting updated file or directory ${filePath}: ${error.message}`);
}
}
ncp.limit = 16;
async function moveFilesFromTemp(basePath, tempExtractFolder, filesInZip) {
const entries = await fs.readdir(tempExtractFolder, { withFileTypes: true });
for (const entry of entries) {
const sourcePath = path.join(tempExtractFolder, entry.name);
const targetPath = path.join(basePath, entry.name);
if (entry.isDirectory()) {
await fs.mkdir(targetPath, { recursive: true });
await ncp(sourcePath, targetPath);
} else {
const relativePath = path.relative(tempExtractFolder, sourcePath);
logger.info(`Processing file: ${relativePath}`);
if (filesInZip.includes(relativePath)) {
const destinationPath = path.join(basePath, relativePath);
await fs.mkdir(path.dirname(destinationPath), { recursive: true });
try {
await fs.rename(sourcePath, destinationPath);
logger.info(`File moved: ${relativePath}`);
} catch (moveError) {
logger.error(`Error moving file: ${relativePath}`, moveError);
}
} else {
logger.info(`Skipped moving file: ${relativePath}`);
}
}
}
}
async function applyUpdate(updateUrl, updateFolder) {
const updateFilePath = path.join(updateFolder, 'update.zip');
try {
await downloadUpdate(updateUrl, updateFilePath);
const filesInZip = await listFilesInZip(updateFilePath);
logger.info('Zip contents:', filesInZip);
const filesInVersion = await getFilesInVersion();
logger.info('Content of current version:', filesInVersion);
for (const file of filesInVersion) {
try {
await deleteFileOrDirectory(path.join(updateFolder, file));
} catch (error) {
ErrorLogger.error(`Error deleting updated file or directory ${file}: ${error.message}`);
}
}
const tempExtractFolder = path.join(updateFolder, 'temp_extract');
await unzipUpdate(updateFilePath, tempExtractFolder);
await moveFilesFromTemp(updateFolder, tempExtractFolder, filesInZip);
logger.info('Update successful. The update has been applied successfully.');
} catch (error) {
ErrorLogger.error(`Error applying update: ${error.message}`);
} finally {
await cleanUp(updateFolder, updateFilePath);
}
}
async function cleanUp(basePath, zipFilePath) {
const tempExtractFolder = path.join(basePath, 'temp_extract');
await fs.rm(tempExtractFolder, { recursive: true });
await fs.unlink(zipFilePath);
logger.info('Temporary files successfully deleted.');
}
async function checkForUpdates() {
try {
logger.info('Checking for updates...');
const response = await fetch('https://apollon.dinawo.fr/api/checkupdate');
const result = await response.json();
if (result.updateAvailable) {
const updateUrl = 'https://apollon.dinawo.fr/api/download/all';
const updateFolder = path.join(__dirname, '..');
await fs.mkdir(updateFolder, { recursive: true });
await applyUpdate(updateUrl, updateFolder);
} else {
logger.info('No updates available.');
}
} catch (error) {
logger.error('Error checking for updates:', error.message);
}
}
module.exports = { applyUpdate, listFilesInZip, checkForUpdates };

8048
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

62
package.json Normal file
View File

@@ -0,0 +1,62 @@
{
"name": "CDN-APP DinawoSR INC",
"version": "1.0.9-beta",
"description": "",
"main": "server.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node server.js"
},
"author": "Dinawo - Group SwiftLogic Labs",
"license": "ISC",
"dependencies": {
"adm-zip": "^0.5.10",
"ansi-to-html": "^0.7.2",
"axios": "^1.6.3",
"axios-debug": "^0.0.4",
"bcrypt": "^5.1.1",
"chalk": "^5.3.0",
"chokidar": "^3.6.0",
"connect-flash": "^0.1.1",
"cookie-parser": "^1.4.6",
"debug": "^4.3.4",
"diff": "^5.1.0",
"dotenv": "^16.3.1",
"ejs": "^3.1.9",
"express": "^4.18.2",
"express-fileupload": "^1.4.0",
"express-progressbar": "^2.0.0",
"express-session": "^1.17.3",
"fs": "^0.0.1-security",
"fs-extra": "^11.2.0",
"ip": "^2.0.1",
"mime-types": "^2.1.35",
"multer": "^1.4.5-lts.1",
"mysql2": "^3.6.3",
"ncp": "^2.0.0",
"node-cron": "^3.0.3",
"node-fetch": "^2.6.6",
"nodemailer": "^6.9.8",
"os-utils": "^0.0.14",
"passport": "^0.6.0",
"passport-activedirectory": "^1.4.0",
"passport-discord": "^0.1.4",
"path": "^0.12.7",
"pg": "^8.11.3",
"pg-promise": "^11.5.4",
"public-ip": "^6.0.1",
"semver": "^7.5.4",
"slugify": "^1.6.6",
"socket.io": "^4.7.2",
"sweetalert2": "^11.10.0",
"systeminformation": "^5.22.0",
"tailwindcss": "^3.3.5",
"toastify-js": "^1.12.0",
"winston": "^3.11.0",
"winston-daily-rotate-file": "^4.7.1"
},
"devDependencies": {
"daisyui": "^4.5.0",
"nodemon": "^3.1.0"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View File

@@ -0,0 +1,15 @@
body{
background-color: black;
color: white;
}
h1 {
color: red;
}
h6{
color: red;
text-decoration: underline;
}

54
public/css/auth.css Normal file
View File

@@ -0,0 +1,54 @@
body {
background-color: #36393f;
color: #ffffff;
font-family: Arial, sans-serif;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
margin: 0;
}
.title-container {
margin-bottom: 20px;
}
.container {
width: 300px;
text-align: center;
}
.tittle {
font-size: 24px;
margin-bottom: 20px;
}
input[type="text"],
input[type="password"] {
width: 100%;
padding: 10px;
margin-bottom: 15px;
background-color: #40444b;
border: none;
color: #ffffff;
border-radius: 5px;
font-size: 16px;
}
button {
background-color: #7289da;
color: #ffffff;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s;
font-size: 16px;
margin: 0 auto;
display: block;
}
button:hover {
background-color: #677bc4;
}

49
public/css/login.css Normal file
View File

@@ -0,0 +1,49 @@
body {
background-color: #f8f9fa;
}
.container {
background-color: #f8f9fa;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
padding: 20px;
margin-top: 50px;
}
.form-group label,
.form-group small {
color: #333;
}
.form-control,
.btn {
border-radius: 20px;
}
.form-group {
margin-bottom: 20px;
}
/* Dark Mode */
body.dark-mode {
background-color: #151a1e;
color: #ffffff;
}
.dark-mode .container {
background-color: #262636;
box-shadow: 0 0 10px rgba(255, 255, 255, 0.1);
}
.dark-mode .form-control {
background-color: #343a40; /* Nouvelle couleur pour les boutons en mode sombre */
border-color: #343a40; /* Nouvelle couleur de bordure pour les boutons en mode sombre */
color: #fff; /* Couleur du texte pour les boutons en mode sombre */
}
.dark-mode .form-group label,
.dark-mode .form-group small {
color: #fff;
}

169
public/css/styles.css Normal file
View File

@@ -0,0 +1,169 @@
.btn-round {
border-radius: 20px;
}
.btn-link {
text-decoration: none !important;
}
.dark-mode {
background-color: #151a1e;
color: #ffffff;
}
.dark-mode table {
color: #ffffff;
}
.dark-mode .navbar-light .navbar-toggler-icon {
background-color: #0b0d0f;
}
.dark-mode .navbar-light .navbar-toggler {
border-color: #0b0d0f;
}
.dark-mode .header {
background-color: #0b0d0f !important;
color: #fff;
}
.dark-mode .navbar-brand {
color: #ffffff;
}
.header-form {
display: flex;
align-items: center;
}
.header-form button {
background-color: #007bff;
border-radius: 20px;
cursor: pointer;
}
.table {
border-radius: 10px;
overflow: hidden;
}
.table th,
.table td {
border: none;
background-color: #f8f9fa;
color: #333;
}
.table th {
background-color: #9798a1;
}
.table-bordered th,
.table-bordered td {
border: 1px solid #dee2e6;
}
.dark-mode .table {
border-radius: 10px;
overflow: hidden;
}
.dark-mode .table th,
.dark-mode .table td {
border: none;
background-color: #262636;
color: #fff;
}
.dark-mode .table th {
background-color: #0b0d0f;
}
.dark-mode .table-bordered th,
.dark-mode .table-bordered td {
border: 1px solid #dee2e6;
}
.table-container {
max-width: 1200px;
margin: 0 auto;
}
.dark-mode .modal-content {
background-color: #262636;
color: #ffffff;
}
.light-mode .modal-content {
background-color: #ffffff;
color: #000000;
}
.modal-content {
border-radius: 15px;
}
.custom-breadcrumb {
font-size: 14px;
max-width: 800px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin: 10px auto 0;
}
.table-hover tbody tr:hover {
background-color: #5b5b82;
}
body.dark-theme .navbar-toggler-icon {
filter: invert(1);
}
#logoutLink {
transition: transform 0.3s ease, background-color 0.3s ease, border-color 0.3s ease;
color: #007BFF;
background-color: transparent;
padding: 8px 16px;
text-decoration: none;
display: inline-block;
font-size: 14px;
margin: 4px 2px;
border-radius: 50px;
cursor: pointer;
box-shadow: 0 2px 5px rgba(0,0,0,0.25);
border: 2px solid #007BFF;
}
#logoutLink:hover {
transform: scale(1.15);
background-color: #007BFF;
color: #fff;
}
.custom-btn {
transition: transform 0.3s ease, background-color 0.3s ease, border-color 0.3s ease;
color: #007BFF;
background-color: transparent;
padding: 8px 16px;
text-decoration: none;
display: inline-block;
font-size: 14px;
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;
}
.btn-icon {
border-radius: 50%;
padding: 0.5em;
}

104
public/css/upload.css Normal file
View File

@@ -0,0 +1,104 @@
body {
background-color: #f8f9fa;
}
.container {
background-color: #f8f9fa;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
padding: 20px;
margin-top: 50px;
}
.progress {
height: 25px;
margin-top: 20px;
}
#progressBar {
background-color: #007bff;
}
.progress-info {
display: flex;
align-items: center;
}
#progressText {
margin-right: 10px;
}
.form-group label,
.form-group small {
color: #333;
}
.form-control,
.btn {
border-radius: 20px;
}
.form-group {
margin-bottom: 20px;
}
body.dark-mode {
background-color: #151a1e;
color: #ffffff;
}
.dark-mode .container{
background-color: #262636;
box-shadow: 0 0 10px rgba(255, 255, 255, 0.1);
}
.progress-bar.dark-mode {
background-color: #007bff;
}
#progressText.dark-mode,
#estimatedTime.dark-mode {
color: #ffffff;
}
.form-group label.dark-mode,
.form-group small.dark-mode {
color: #fff;
}
.dark-mode .form-control.dark-mode,
.btn.dark-mode {
background-color: #0b0d0f;
border-color: #0b0d0f;
color: #fff;
}
.dark-mode .form-group label{
color: #fff;
}
.dark-mode text-muted{
color: #fff;
}
.custom-btn {
transition: transform 0.3s ease, background-color 0.3s ease, border-color 0.3s ease;
color: #007BFF;
background-color: transparent;
padding: 8px 16px;
text-decoration: none;
display: inline-block;
font-size: 14px;
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;
}

536
public/js/dashboard.js Normal file
View File

@@ -0,0 +1,536 @@
document.addEventListener('DOMContentLoaded', function () {
const copyButtons = document.querySelectorAll('.copy-button');
copyButtons.forEach(copyButton => {
copyButton.addEventListener("click", () => {
const fileContainer = copyButton.closest('tr');
const fileLink = fileContainer.querySelector('.file-link');
fileLink.style.display = "block";
fileLink.select();
document.execCommand("copy");
fileLink.style.display = "none";
copyButton.textContent = "Lien copié !";
setTimeout(() => {
copyButton.textContent = "Copier le lien";
}, 2000);
});
});
const filterForm = document.getElementById('filterForm');
const extensionFilter = document.getElementById('extensionFilter');
const fileSearchInput = document.getElementById('fileSearch');
const styleSwitcherButton = document.getElementById('styleSwitcher');
const icon = document.getElementById('themeIcon');
const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
let isDarkMode = darkModeMediaQuery.matches;
function toggleDarkMode() {
isDarkMode = !isDarkMode;
document.body.classList.toggle('dark-mode', isDarkMode);
if (isDarkMode) {
icon.classList.remove('bi-brightness-high-fill');
icon.classList.add('bi-moon-fill');
icon.innerHTML = '</svg><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-brightness-high-fill" viewBox="0 0 16 16"><path d="M12 8a4 4 0 1 1-8 0 4 4 0 0 1 8 0M8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0m0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13m8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5M3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8m10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0m-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0m9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707M4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708"/></svg>';
} else {
icon.classList.remove('bi-moon-fill');
icon.classList.add('bi-brightness-high-fill');
icon.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-moon-fill" viewBox="0 0 16 16"><path d="M6 .278a.77.77 0 0 1 .08.858 7.2 7.2 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277q.792-.001 1.533-.16a.79.79 0 0 1 .81.316.73.73 0 0 1-.031.893A8.35 8.35 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.75.75 0 0 1 6 .278"/>';
}
}
function applyStyleMode() {
document.body.classList.toggle('dark-mode', isDarkMode);
if (isDarkMode) {
icon.classList.remove('bi-brightness-high-fill');
icon.classList.add('bi-moon-fill');
icon.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-brightness-high-fill" viewBox="0 0 16 16"><path d="M12 8a4 4 0 1 1-8 0 4 4 0 0 1 8 0M8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0m0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13m8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5M3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8m10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0m-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0m9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707M4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708"/></svg>';
} else {
icon.classList.remove('bi-moon-fill');
icon.classList.add('bi-brightness-high-fill');
icon.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-moon-fill" viewBox="0 0 16 16"><path d="M6 .278a.77.77 0 0 1 .08.858 7.2 7.2 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277q.792-.001 1.533-.16a.79.79 0 0 1 .81.316.73.73 0 0 1-.031.893A8.35 8.35 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.75.75 0 0 1 6 .278"/></svg>';
}
}
darkModeMediaQuery.addListener(applyStyleMode);
applyStyleMode();
styleSwitcherButton.addEventListener('click', toggleDarkMode);
filterForm.addEventListener('submit', function (event) {
event.preventDefault();
const selectedExtension = extensionFilter.value.toLowerCase();
const searchQuery = fileSearchInput.value.toLowerCase();
const fileList = document.querySelectorAll('tr[data-extension]');
fileList.forEach(file => {
const fileExtension = file.getAttribute('data-extension').toLowerCase();
const fileName = file.querySelector('td:first-child').textContent.toLowerCase();
const extensionMatch = selectedExtension === '' || selectedExtension === fileExtension;
const searchMatch = fileName.includes(searchQuery);
if (extensionMatch && searchMatch) {
file.style.display = '';
} else {
file.style.display = 'none';
}
});
});
});
document.addEventListener('DOMContentLoaded', function () {
const deleteFolderButtons = document.querySelectorAll('.delete-folder-button');
deleteFolderButtons.forEach(deleteButton => {
deleteButton.addEventListener('click', async () => {
const fileContainer = deleteButton.closest('tr');
const folderName = fileContainer.querySelector('td:first-child').textContent;
Swal.fire({
title: 'Êtes-vous sûr?',
text: `La suppression du dossier "${folderName}" est irréversible!`,
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#d33',
cancelButtonColor: '#3085d6',
confirmButtonText: 'Supprimer',
cancelButtonText: 'Annuler',
}).then(async (result) => {
if (result.isConfirmed) {
try {
const response = await fetch(`/dpanel/dashboard/deletefolder/${folderName}`, {
method: 'DELETE',
});
if (response.ok) {
fileContainer.remove();
Swal.fire({
position: 'top',
icon: 'success',
title: 'Le dossier a été supprimé avec succès.',
showConfirmButton: false,
timer: 1800,
toast: true
});
} else {
Swal.fire({
position: 'top',
icon: 'error',
title: 'La suppression du dossier a échoué',
showConfirmButton: false,
timer: 1800,
toast: true
});
}
} catch (error) {
console.error('Erreur lors de la suppression du dossier:', error);
Swal.fire('Erreur!', 'Une erreur s\'est produite lors de la suppression du dossier.', 'error');
}
}
});
});
});
});
window.onload = function() {
var newFolderModalBtn = document.getElementById('newFolderModalBtn');
if(newFolderModalBtn) {
newFolderModalBtn.addEventListener('click', function (e) {
e.preventDefault();
Swal.fire({
title: 'Nouveau dossier',
input: 'text',
inputPlaceholder: 'Entrer le nom du nouveau dossier',
confirmButtonText: 'Créer',
showCancelButton: true,
cancelButtonText: 'Annuler',
preConfirm: (folderName) => {
if (!folderName) {
Swal.showValidationMessage('Le nom du dossier ne peut pas être vide.');
}
return folderName;
}
}).then(result => {
if (result.isConfirmed) {
const folderName = result.value.trim();
fetch('/dpanel/dashboard/newfolder', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ folderName }),
})
.then(response => {
if (response.ok) {
return response.json();
} else {
return response.json().then(error => Promise.reject(error));
}
})
.then(result => {
Swal.fire({
position: 'top',
icon: 'success',
title: 'Le dossier a été créé avec succès.',
showConfirmButton: false,
timer: 2000,
toast: true
})
.then(() => {
location.reload();
});
})
.catch(error => {
Swal.fire({
position: 'top',
icon: 'error',
title: 'Erreur lors de la création du dossier.',
text: error.message,
showConfirmButton: false,
timer: 2350,
toast: true
})
});
}
});
});
}
};
function performUpdate() {
fetch('/applyupdate')
.then(response => response.json())
.then(result => {
if (result.success) {
Swal.fire({
title: 'Mise à jour réussie',
text: 'Votre application a été mise à jour avec succès.',
icon: 'success',
toast: true,
position: 'bottom-right',
showConfirmButton: false,
timer: 5000
});
} else {
Swal.fire({
title: 'Mise à jour échouée',
text: 'Une erreur s\'est produite lors de la mise à jour de votre application.',
icon: 'error',
toast: true,
position: 'bottom-right',
showConfirmButton: false,
timer: 5000
});
}
})
.catch(error => {
console.error('Erreur lors de la mise à jour :', error);
Swal.fire({
text: 'Erreur lors de la mise à jour.',
icon: 'error',
toast: true,
position: 'bottom-right',
showConfirmButton: false,
timer: 5000
});
});
}
function checkUpdates() {
fetch('/checkupdate')
.then(response => response.json())
.then(result => {
if (result.updateAvailable) {
Swal.fire({
title: 'Nouvelle mise à jour disponible',
text: 'Voulez-vous mettre à jour votre application?',
icon: 'info',
showCancelButton: true,
confirmButtonText: 'Oui',
cancelButtonText: 'Non',
position: 'bottom-right',
toast: true,
}).then((result) => {
if (result.isConfirmed) {
performUpdate();
}
});
} else {
Swal.fire({
title: 'Application à jour',
text: 'Votre application est à jour.',
icon: 'success',
toast: true,
position: 'bottom-right',
showConfirmButton: false,
timer: 5000
});
}
})
.catch(error => {
console.error('Erreur lors de la vérification des mises à jour :', error);
Swal.fire({
text: 'Erreur lors de la vérification des mises à jour.',
icon: 'error',
toast: true,
position: 'bottom-right',
showConfirmButton: false,
timer: 5000
});
});
}
document.getElementById('checkUpdateButton').addEventListener('click', async function() {
const userName = await getLoggedInUserName();
fs.readFile('user.json', (err, data) => {
if (err) throw err;
let users = JSON.parse(data);
const user = users.find(user => user.name === userName);
if (user && user.role === 'admin') {
checkUpdates();
} else {
Swal.fire({
position: 'top',
icon: 'warning',
title: 'Vous n\'avez pas les droits pour effectuer cette action.',
showConfirmButton: false,
timer: 2600,
toast: true
});
}
});
});
var modal = document.getElementById('patchNoteModal');
if (isDarkMode()) {
modal.classList.add('dark-mode');
} else {
modal.classList.add('light-mode');
}
$(document).ready(function () {
$('#accountDropdownBtn').on('click', function () {
$('#accountDropdownMenu').toggleClass('show');
});
$(document).on('click', function (e) {
if (!$('#accountDropdownBtnGroup').is(e.target) && $('#accountDropdownBtnGroup').has(e.target).length === 0) {
$('#accountDropdownMenu').removeClass('show');
}
});
});
function confirmDelete(filename) {
Swal.fire({
title: 'Êtes-vous sûr de vouloir supprimer ce fichier?',
text: 'Cette action est irréversible!',
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#d33',
cancelButtonColor: '#3085d6',
confirmButtonText: 'Supprimer'
}).then(async (result) => {
if (result.isConfirmed) {
try {
const response = await fetch('/dpanel/dashboard/delete', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
filename: filename,
}),
});
if (response.ok) {
Swal.fire({
position: 'top',
icon: 'success',
title: 'Le fichier a été supprimé avec succès.',
showConfirmButton: false,
timer: 1800,
toast: true
})
.then(() => {
location.reload();
});
} else {
Swal.fire({
position: 'top',
icon: 'error',
title: 'La suppression du fichier a échoué.',
showConfirmButton: false,
timer: 1800,
toast: true
});
}
} catch (error) {
console.error('Erreur lors de la suppression du fichier:', error);
Swal.fire('Erreur!', 'Une erreur s\'est produite lors de la suppression du fichier.', 'error');
}
}
});
}
function renameFile(folderName, currentName) {
const fileExtensionIndex = currentName.lastIndexOf('.');
const fileExtension = currentName.substring(fileExtensionIndex);
Swal.fire({
title: 'Entrez le nouveau nom',
input: 'text',
inputValue: currentName,
inputPlaceholder: 'Nouveau nom',
showCancelButton: true,
confirmButtonText: 'Renommer',
cancelButtonText: 'Annuler',
onOpen: (el) => {
setTimeout(() => {
const input = Swal.getInput();
const pos = input.value.lastIndexOf('.');
input.setSelectionRange(0, pos);
}, 0);
}
}).then((result) => {
if (result.isConfirmed) {
const newName = result.value;
fetch(`/dpanel/dashboard/rename/${folderName}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ currentName: currentName, newName: newName }),
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.text();
})
.then(data => {
Swal.fire({
position: 'top',
icon: 'success',
title: 'Le fichier a été renommé avec succès.',
showConfirmButton: false,
timer: 1800,
toast: true,
}).then(() => {
location.reload();
});
})
.catch((error) => {
Swal.fire({
position: 'top',
icon: 'error',
title: 'Erreur lors du renommage du fichier.',
showConfirmButton: false,
timer: 1800,
toast: true,
});
});
}
});
}
async function getUserIdFromFolder(username) {
if (!username) {
console.error('Username is not defined');
return null;
}
let users;
try {
let response = await fetch(`/user.json`);
if (!response.ok) {
throw new Error('Network response was not ok');
}
users = await response.json();
} catch (error) {
console.error('Error:', error);
return null;
}
let user = users.find(user => user.name.toLowerCase() === username.toLowerCase());
return user ? user.id : null;
}
async function showFileInfo(fileLink) {
let data;
try {
let response = await fetch(`/file_info.json`);
if (!response.ok) {
throw new Error('Network response was not ok');
}
data = await response.json();
} catch (error) {
Swal.fire({
position: 'top',
icon: 'error',
title: 'Les informations sur les fichiers ne sont pas disponibles pour le moment. Veuillez réessayer plus tard.',
showConfirmButton: false,
timer: 1800,
toast: true,
});
return;
}
let fileName = fileLink.split('/').pop();
let pathParts = fileLink.split('/');
if (pathParts.length < 2) {
Swal.fire({
position: 'top',
icon: 'error',
title: `Le lien du fichier ${fileLink} n'est pas valide.`,
showConfirmButton: false,
timer: 1800,
toast: true,
});
return;
}
let username = pathParts[1].replace('.', '');
console.log('Extracted username:', username);
let userId = await getUserIdFromFolder(username);
console.log('Obtained userId:', userId);
let fileInfo = data.find(file => file.fileName === fileName && file.Id === userId);
if (!fileInfo) {
Swal.fire({
position: 'top',
icon: 'error',
title: `Aucune information trouvée pour le fichier ${fileName} pour l'utilisateur ${username}.`,
showConfirmButton: false,
timer: 1800,
toast: true,
});
return;
}
console.log('Found fileInfo:', fileInfo);
let html = `<p>Nom du fichier : ${fileInfo.fileName}</p>`;
if (fileInfo.expiryDate) {
html += `<p>Date de fin de disponibilité : ${fileInfo.expiryDate}</p>`;
}
if (fileInfo.password) {
html += `<p>Mot de passe : Oui</p>`;
}
Swal.fire({
title: 'Informations sur le fichier',
html: html,
confirmButtonText: 'Fermer'
});
}

340
public/js/folder.js Normal file
View File

@@ -0,0 +1,340 @@
function calculateFolderSize(contents) {
let totalSize = 0;
contents.forEach(file => {
if (file.type === 'file' && file.size !== undefined && !isNaN(file.size) && file.size >= 0) {
totalSize += file.size;
} else if (file.type === 'folder' && file.contents !== undefined) {
totalSize += calculateFolderSize(file.contents);
}
});
return totalSize;
}
document.addEventListener('DOMContentLoaded', function () {
const copyButtons = document.querySelectorAll('.copy-button');
copyButtons.forEach(copyButton => {
copyButton.addEventListener("click", () => {
const fileContainer = copyButton.closest('tr');
const fileLink = fileContainer.querySelector('.file-link');
fileLink.style.display = "block";
fileLink.select();
document.execCommand("copy");
fileLink.style.display = "none";
copyButton.textContent = "Lien copié !";
setTimeout(() => {
copyButton.textContent = "Copier le lien";
}, 2000);
});
});
const filterForm = document.getElementById('filterForm');
const extensionFilter = document.getElementById('extensionFilter');
const fileSearchInput = document.getElementById('fileSearch');
const styleSwitcherButton = document.getElementById('styleSwitcher');
const icon = document.getElementById('themeIcon');
const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
let isDarkMode = darkModeMediaQuery.matches;
function toggleDarkMode() {
isDarkMode = !isDarkMode;
document.body.classList.toggle('dark-mode', isDarkMode);
if (isDarkMode) {
icon.classList.remove('bi-brightness-high-fill');
icon.classList.add('bi-moon-fill');
icon.innerHTML = '</svg><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-brightness-high-fill" viewBox="0 0 16 16"><path d="M12 8a4 4 0 1 1-8 0 4 4 0 0 1 8 0M8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0m0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13m8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5M3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8m10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0m-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0m9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707M4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708"/></svg>';
} else {
icon.classList.remove('bi-moon-fill');
icon.classList.add('bi-brightness-high-fill');
icon.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-moon-fill" viewBox="0 0 16 16"><path d="M6 .278a.77.77 0 0 1 .08.858 7.2 7.2 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277q.792-.001 1.533-.16a.79.79 0 0 1 .81.316.73.73 0 0 1-.031.893A8.35 8.35 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.75.75 0 0 1 6 .278"/>';
}
}
function applyStyleMode() {
document.body.classList.toggle('dark-mode', isDarkMode);
if (isDarkMode) {
icon.classList.remove('bi-brightness-high-fill');
icon.classList.add('bi-moon-fill');
icon.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-brightness-high-fill" viewBox="0 0 16 16"><path d="M12 8a4 4 0 1 1-8 0 4 4 0 0 1 8 0M8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0m0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13m8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5M3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8m10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0m-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0m9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707M4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708"/></svg>';
} else {
icon.classList.remove('bi-moon-fill');
icon.classList.add('bi-brightness-high-fill');
icon.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-moon-fill" viewBox="0 0 16 16"><path d="M6 .278a.77.77 0 0 1 .08.858 7.2 7.2 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277q.792-.001 1.533-.16a.79.79 0 0 1 .81.316.73.73 0 0 1-.031.893A8.35 8.35 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.75.75 0 0 1 6 .278"/></svg>';
}
}
darkModeMediaQuery.addListener(applyStyleMode);
applyStyleMode();
styleSwitcherButton.addEventListener('click', toggleDarkMode);
filterForm.addEventListener('submit', function (event) {
event.preventDefault();
const selectedExtension = extensionFilter.value.toLowerCase();
const searchQuery = fileSearchInput.value.toLowerCase();
const fileList = document.querySelectorAll('tr[data-extension]');
fileList.forEach(file => {
const fileExtension = file.getAttribute('data-extension').toLowerCase();
const fileName = file.querySelector('td:first-child').textContent.toLowerCase();
const extensionMatch = selectedExtension === '' || selectedExtension === fileExtension;
const searchMatch = fileName.includes(searchQuery);
if (extensionMatch && searchMatch) {
file.style.display = '';
} else {
file.style.display = 'none';
}
});
});
});
async function confirmDeleteFile(folderName, filename) {
const requestBody = JSON.stringify({
filename: filename,
});
const confirmationResult = await Swal.fire({
title: 'Êtes-vous sûr de vouloir supprimer ce fichier?',
text: 'Cette action est irréversible!',
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#d33',
cancelButtonColor: '#3085d6',
confirmButtonText: 'Supprimer'
});
if (confirmationResult.isConfirmed) {
try {
const response = await fetch(`/dpanel/dashboard/deletefile/${folderName}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: requestBody,
});
const responseData = await response.json();
if (response.ok) {
handleDeleteSuccess();
} else {
handleDeleteFailure(response, responseData);
}
} catch (error) {
console.error('Erreur lors de la suppression du fichier:', error);
handleDeleteFailure();
}
}
}
function handleDeleteSuccess() {
Swal.fire({
position: 'top',
icon: 'success',
title: 'Le fichier a été supprimé avec succès.',
showConfirmButton: false,
timer: 1800,
toast: true
})
.then(() => {
location.reload();
});
}
function handleDeleteFailure(response, responseData) {
if (response.status === 404 && responseData && responseData.status === 'error') {
Swal.fire({
position: 'top',
icon: 'error',
title: responseData.message,
showConfirmButton: false,
timer: 1800,
toast: true
})
} else {
Swal.fire({
position: 'top',
icon: 'error',
title: 'La suppression du fichier a échoué.',
showConfirmButton: false,
timer: 1800,
toast: true
})
}
}
function getCurrentFolderName() {
const currentPath = window.location.pathname;
const pathSegments = currentPath.split('/');
const currentFolderName = pathSegments[pathSegments.length - 1];
return currentFolderName;
}
function renameFolder(currentName, newName) {
try {
const currentFolderName = getCurrentFolderName();
const renameURL = `/dpanel/dashboard/renamefile/${currentFolderName}`;
fetch(renameURL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ newName: newName, currentName: currentName }),
})
.then(response => response.json())
.then(data => {
if (data.renamed) {
Swal.fire({
position: 'top',
icon: 'errsuccessor',
title: 'Succès',
showConfirmButton: false,
timer: 1800,
toast: true,
}).then(() => {
location.reload();
});
} else {
throw new Error(data.error);
}
})
.catch(error => {
console.error('Erreur lors du renommage du fichier:', error);
Swal.fire({
position: 'top',
icon: 'error',
title: error.message || 'Une erreur est survenue lors du renommage du fichier.',
showConfirmButton: false,
timer: 1800,
toast: true
})
});
} catch (error) {
console.error('Erreur lors du renommage du fichier:', error);
}
}
function renameFile(folderName, currentName) {
const fileExtensionIndex = currentName.lastIndexOf('.');
const fileExtension = currentName.substring(fileExtensionIndex);
Swal.fire({
title: 'Entrez le nouveau nom',
input: 'text',
inputValue: currentName,
inputPlaceholder: 'Nouveau nom',
showCancelButton: true,
confirmButtonText: 'Renommer',
cancelButtonText: 'Annuler',
onOpen: (el) => {
setTimeout(() => {
const input = Swal.getInput();
const pos = input.value.lastIndexOf('.');
input.setSelectionRange(0, pos);
}, 0);
}
}).then((result) => {
if (result.isConfirmed) {
const newName = result.value;
fetch(`/dpanel/dashboard/rename/${folderName}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ currentName: currentName, newName: newName }),
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.text();
})
.then(data => {
Swal.fire({
position: 'top',
icon: 'success',
title: 'Le fichier a été renommé avec succès.',
showConfirmButton: false,
timer: 1800,
toast: true,
}).then(() => {
location.reload();
});
})
.catch((error) => {
Swal.fire({
position: 'top',
icon: 'error',
title: 'Erreur lors du renommage du fichier.',
showConfirmButton: false,
timer: 1800,
toast: true,
});
});
}
});
}
async function showFileInfo(fileName) {
let data;
try {
let response = await fetch('/file_info.json');
if (!response.ok) {
throw new Error('Network response was not ok');
}
data = await response.json();
} catch (error) {
Swal.fire({
position: 'top',
icon: 'error',
title: 'Les informations sur les fichiers ne sont pas disponibles pour le moment. Veuillez réessayer plus tard.',
showConfirmButton: false,
timer: 1800,
toast: true,
});
return;
}
let fileInfo = data.find(file => file.fileName === fileName);
if (!fileInfo) {
Swal.fire({
position: 'top',
icon: 'error',
title: `Aucune information trouvée pour le fichier ${fileName}.`,
showConfirmButton: false,
timer: 1800,
toast: true,
});
return;
}
let html = `<p>Nom du fichier : ${fileInfo.fileName}</p>`;
if (fileInfo.expiryDate) {
html += `<p>Date de fin de disponibilité : ${fileInfo.expiryDate}</p>`;
}
if (fileInfo.password) {
html += `<p>Mot de passe : Oui</p>`;
}
Swal.fire({
title: 'Informations sur le fichier',
html: html,
confirmButtonText: 'Fermer'
});
}

194
routes/attachments.js Normal file
View File

@@ -0,0 +1,194 @@
const express = require('express');
const router = express.Router();
const path = require('path');
const fs = require('fs').promises;
const mime = require('mime-types');
const { logger, ErrorLogger } = require('../config/logs');
const crypto = require('crypto');
const baseDir = 'cdn-files';
async function getSamAccountNameFromUserId(userId) {
const data = await fs.readFile(path.join(__dirname, '../user.json'), 'utf8');
const users = JSON.parse(data);
const user = users.find(user => user.id === userId);
if (user) {
return user.name;
} else {
throw new Error('User not found');
}
}
async function findFileInUserDir(userId, filename) {
const samaccountname = await getSamAccountNameFromUserId(userId);
const userDir = path.join(baseDir, samaccountname);
return findFileInDir(userDir, filename);
}
async function findFileInDir(dir, filename) {
const files = await fs.readdir(dir, { withFileTypes: true });
for (const file of files) {
const filePath = path.join(dir, file.name);
if (file.name === filename && file.isFile()) {
return filePath;
} else if (file.isDirectory()) {
const found = await findFileInDir(filePath, filename);
if (found) {
return found;
}
}
}
return null;
}
router.get('/:userId', (req, res) => {
res.render('unauthorized');
});
router.get('/:userId/:filename', async (req, res) => {
const { userId, filename } = req.params;
try {
const filePath = await findFileInUserDir(userId, filename);
if (!filePath) {
return res.render('file-not-found');
}
const data = await fs.readFile('file_info.json', 'utf8');
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();
if (expiryDate < now) {
await fs.unlink(filePath);
return res.render('file-expired');
}
if (fileInfo.password && !req.session.passwordVerified) {
return res.render('password-check', { userId, filename });
}
}
const fileContent = await fs.readFile(filePath);
let mimeType = mime.lookup(filePath);
if (!mimeType) {
if (filePath.endsWith('.txt')) {
mimeType = 'text/plain';
} else if (filePath.endsWith('.pdf')) {
mimeType = 'application/pdf';
}
}
if (mimeType) {
res.setHeader('Content-Type', mimeType);
}
if (mimeType === 'text/plain') {
res.end(fileContent);
} else {
res.send(fileContent);
}
if (fileInfo) {
req.session.passwordVerified = false;
}
} catch (err) {
ErrorLogger.error('Error reading file:', err);
return res.status(500).send('Error reading file.');
}
});
router.post('/:userId/:filename', async (req, res) => {
const { userId, filename } = req.params;
const enteredPassword = req.body.password;
try {
const data = await fs.readFile('file_info.json', 'utf8');
const fileInfoArray = JSON.parse(data);
const fileInfo = fileInfoArray.find(info => info.fileName === filename && info.Id === userId);
if (!fileInfo) {
return res.json({ success: false, message: 'File not found' });
}
const algorithm = 'aes-256-cbc';
const key = crypto.scryptSync(enteredPassword, 'salt', 32);
const iv = Buffer.alloc(16, 0);
const cipher = crypto.createCipheriv(algorithm, key, iv);
const encryptedPassword = cipher.update('', 'utf8', 'hex') + cipher.final('hex');
if (fileInfo.password === encryptedPassword) {
req.session.passwordVerified = true;
const filePath = await findFileInUserDir(userId, filename);
const fileContent = await fs.readFile(filePath);
let mimeType = mime.lookup(filePath);
if (!mimeType) {
if (filePath.endsWith('.txt')) {
mimeType = 'text/plain';
} else if (filePath.endsWith('.pdf')) {
mimeType = 'application/pdf';
}
}
res.json({ success: true, fileContent: fileContent.toString('base64'), 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.');
}
});
async function deleteExpiredFiles() {
let data = await fs.readFile('file_info.json', 'utf8');
let fileInfoArray = JSON.parse(data);
const now = new Date();
let newFileInfoArray = [];
for (let i = 0; i < fileInfoArray.length; i++) {
const fileInfo = fileInfoArray[i];
let expiryDate;
if (fileInfo.expiryDate && fileInfo.expiryDate.trim() !== '') {
expiryDate = new Date(fileInfo.expiryDate);
} else {
continue;
}
if (expiryDate < now) {
const samaccountname = await getSamAccountNameFromUserId(fileInfo.userId);
const userDir = path.join(baseDir, samaccountname);
const filePath = path.join(userDir, fileInfo.fileName);
try {
await fs.unlink(filePath);
} catch (err) {
ErrorLogger.error('Error deleting file:', err);
}
} else {
newFileInfoArray.push(fileInfo);
}
}
try {
await fs.writeFile('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;

110
routes/auth.js Normal file
View File

@@ -0,0 +1,110 @@
const express = require('express');
const router = express.Router();
const passport = require('passport');
const fs = require('fs');
const { checkUserExistsAD } = require('../Middlewares/UserIDMiddlewareAD');
const { checkUserExistsDiscord } = require('../Middlewares/UserIDMiddlewareDiscord');
const path = require('path');
const { getUserData, getSetupData } = require('../Middlewares/watcherMiddleware');
let setupData = getSetupData();
let userData = getUserData();
let adStrategy;
if (setupData.ldap !== undefined) {
adStrategy = require('../models/Passport-ActiveDirectory');
}
let DiscordStrategy;
if (setupData.discord !== undefined) {
DiscordStrategy = require('../models/Passport-Discord');
}
let user = userData;
if (user.identifyURL) {
app.get("/auth/discord", (req, res) => {
res.redirect(user.identifyURL);
});
} else {
}
router.use(passport.initialize());
router.use(passport.session());
router.get('/login', function(req, res) {
const setupFilePath = path.join('setup.json');
const setupData = JSON.parse(fs.readFileSync(setupFilePath, 'utf-8'));
res.render('AuthLogin', { setupData, isAuthenticated: false, errorMessage: '', showActiveDirectoryForm: true, currentUrl: req.originalUrl });
});
passport.deserializeUser((user, done) => {
done(null, user);
});
router.get('/logout', (req, res) => {
req.logout(function(err) {
if (err) {
return next(err);
}
res.redirect('/auth/login');
});
});
var opts = { failWithError: true }
router.post('/activedirectory', (req, res, next) => {
passport.authenticate('ActiveDirectory', opts, (err, user, info) => {
if (err) {
return res.render('AuthLogin', { isAuthenticated: false, errorMessage: err.message, setupData: {}, showActiveDirectoryForm: true, currentUrl: req.originalUrl });
}
if (!user) {
return res.render('AuthLogin', { isAuthenticated: false, errorMessage: 'L\'utilisateur n\'est pas autorisé.', setupData: {}, showActiveDirectoryForm: true, currentUrl: req.originalUrl });
}
req.user = {
...user._json,
name: user._json.sAMAccountName,
id: user._json.sAMAccountName,
};
req.logIn(req.user, function(err) {
if (err) { return next(err); }
return next();
});
})(req, res, next);
}, checkUserExistsAD);
router.get("/discord", (req, res) => {
res.redirect(setupData.discord.identifyURL);
});
router.get('/discord/callback', (req, res, next) => {
passport.authenticate('discord', (err, user, info) => {
if (err) {
return next(err);
}
if (!user) {
return res.redirect('/auth/login');
}
req.logIn(user, (loginErr) => {
if (loginErr) {
return next(loginErr);
}
checkUserExistsDiscord(req, res, () => {
if (req.userExists) {
return res.redirect('/dpanel/dashboard');
} else {
createUser(req.user, (createErr) => {
if (createErr) {
return next(createErr);
}
return res.redirect('/dpanel/dashboard');
});
}
});
});
})(req, res, next);
});
module.exports = router;

698
routes/dpanel.js Normal file
View File

@@ -0,0 +1,698 @@
const express = require('express');
const fs = require('fs');
const path = require('path');
const router = express.Router();
const fileUpload = require('express-fileupload');
const authMiddleware = require('../Middlewares/authMiddleware');
const { loggers } = require('winston');
const ncp = require('ncp').ncp;
const configFile = fs.readFileSync('setup.json');
const config = JSON.parse(configFile);
const bodyParser = require('body-parser');
const crypto = require('crypto');
const os = require('os');
const { getUserData, getSetupData } = require('../Middlewares/watcherMiddleware');
let setupData = getSetupData();
let userData = getUserData();
router.use(bodyParser.json());
router.get('/dashboard', authMiddleware, async (req, res) => {
const folderName = req.params.folderName || '';
if (!req.userData || !req.userData.name) {
return res.render('error-recovery-file', { error: 'User data is undefined or incomplete' });
}
const userId = req.userData.id;
const userName = req.userData.name;
const downloadDir = path.join('cdn-files', userName);
const domain = config.domain || 'mydomain.com';
if (!fs.existsSync(downloadDir)) {
fs.mkdirSync(downloadDir, { recursive: true });
}
try {
fs.accessSync(downloadDir, fs.constants.R_OK | fs.constants.W_OK);
} catch (err) {
console.error('No access!', err);
return res.render('error-recovery-file', { error: 'No access to directory' });
}
let fileInfoNames = [];
try {
const fileInfo = JSON.parse(fs.readFileSync('file_info.json', 'utf8'));
fileInfoNames = fileInfo.map(file => file.fileName);
} catch (err) {
console.error('Error reading file_info.json:', err);
}
try {
const files = await fs.promises.readdir(downloadDir);
const folders = files.filter(file => fs.statSync(path.join(downloadDir, file)).isDirectory());
const fileDetails = files.map(file => {
const filePath = path.join(downloadDir, file);
const stats = fs.statSync(filePath);
const fileExtension = path.extname(file).toLowerCase();
const encodedFileName = encodeURIComponent(file);
const fileLink = `https://${domain}/attachments/${userId}/${encodedFileName}`;
const fileType = stats.isDirectory() ? 'folder' : 'file';
return {
name: file,
size: stats.size,
url: fileLink,
extension: fileExtension,
type: fileType
};
});
const availableExtensions = Array.from(new Set(fileDetails.map(file => file.extension)));
res.render('dashboard', { files: fileDetails, folders, extensions: availableExtensions, allFolders: folders, folderName: folderName, fileInfoNames: fileInfoNames });
} catch (err) {
console.error('Error reading directory:', err);
return res.render('error-recovery-file', { error: err.message });
}
});
router.get('/dashboard/folder/:folderName', authMiddleware, async (req, res) => {
const userId = req.userData.name;
const folderName = req.params.folderName || '';
const folderPath = path.join('cdn-files', userId, folderName);
const userFolderPath = path.join('cdn-files', userId);
const domain = config.domain || 'mydomain.com';
const currentFolderName = folderName || '';
const data = await fs.promises.readFile('user.json', 'utf8');
const users = JSON.parse(data);
const user = users.find(user => user.name === userId);
if (!user) {
return res.status(500).send('User not found in user.json');
}
const userRealId = user.id;
const fileInfoData = await fs.promises.readFile('file_info.json', 'utf8');
const fileInfo = JSON.parse(fileInfoData);
const fileInfoNames = fileInfo.map(file => file.fileName);
fs.readdir(folderPath, { withFileTypes: true }, (err, entries) => {
if (err) {
console.error('Error reading directory:', err);
return res.render('error-recovery-file');
}
const folders = entries
.filter(entry => entry.isDirectory())
.map(entry => entry.name);
fs.readdir(userFolderPath, { withFileTypes: true }, (err, allEntries) => {
if (err) {
console.error('Error reading user directory:', err);
return res.render('error-recovery-file');
}
const allFolders = allEntries
.filter(entry => entry.isDirectory())
.map(entry => entry.name);
const fileDetailsPromises = entries.map(entry => {
const filePath = path.join(folderPath, entry.name);
return new Promise((resolve, reject) => {
fs.stat(filePath, (err, stats) => {
if (err) {
console.error('Error getting file stats:', err);
return reject(err);
}
const encodedFileName = encodeURIComponent(entry.name);
const fileLink = `https://${domain}/attachments/${userRealId}/${encodedFileName}`;
const fileType = entry.isDirectory() ? 'folder' : 'file';
resolve({
name: entry.name,
size: stats.size,
url: fileLink,
extension: path.extname(entry.name).toLowerCase(),
type: fileType
});
});
});
});
Promise.all(fileDetailsPromises)
.then(fileDetails => {
const availableExtensions = Array.from(new Set(fileDetails.map(file => file.extension)));
res.render('folder', { files: fileDetails, folders, allFolders, extensions: availableExtensions, currentFolder: currentFolderName, folderName: folderName, fileInfoNames });
})
.catch(error => {
console.error('Error processing file details:', error);
res.status(500).send('Erreur lors du traitement des détails des fichiers.');
});
});
});
});
router.post('/dashboard/newfolder', authMiddleware, (req, res) => {
try {
console.log('Received POST request to create a new folder.');
const userId = req.userData.name;
let { folderName } = req.body;
console.log('Received folderName:', folderName);
if (!folderName || typeof folderName !== 'string') {
console.log('Invalid folderName:', folderName);
return res.status(400).json({ message: 'Le nom du dossier ne peut pas être vide.' });
}
folderName = folderName.trim();
if (!folderName) {
console.log('Trimmed folderName is empty.');
return res.status(400).json({ message: 'Le nom du dossier ne peut pas être vide.' });
}
const folderPath = path.join('cdn-files', userId, folderName);
if (fs.existsSync(folderPath)) {
console.log('Folder already exists:', folderPath);
return res.status(400).json({ message: 'Le dossier existe déjà.' });
}
fs.mkdir(folderPath, (err) => {
if (err) {
console.error(err);
return res.status(500).json({ message: 'Erreur lors de la création du dossier.', error: err });
}
console.log('Folder created successfully:', folderPath);
res.status(200).json({ message: 'Dossier créé avec succès.' });
});
} catch (error) {
console.error('Error creating folder:', error);
return res.status(500).json({ message: 'Erreur lors de la création du dossier.', error: error });
}
});
router.post('/dashboard/rename', authMiddleware, async (req, res) => {
const userId = req.userData.name;
const { currentName, newName } = req.body;
if (!currentName || !newName) {
return res.status(400).send('Both currentName and newName must be provided.');
}
const currentPath = path.join('cdn-files', userId || '', currentName);
const newPath = path.join('cdn-files', userId, newName);
try {
await fs.promises.rename(currentPath, newPath);
const data = await fs.promises.readFile('file_info.json', 'utf8');
let fileInfo = JSON.parse(data);
let found = false;
for (let i = 0; i < fileInfo.length; i++) {
if (fileInfo[i].fileName === currentName) {
fileInfo[i].fileName = newName;
found = true;
break;
}
}
if (found) {
await fs.promises.writeFile('file_info.json', JSON.stringify(fileInfo, null, 2), 'utf8');
}
res.status(200).send('L\'opération a été effectuée avec succès.');
} catch (err) {
console.error(err);
return res.status(500).send('Erreur lors du changement de nom du fichier.');
}
});
router.post('/dashboard/rename/:filePath*', authMiddleware, async (req, res) => {
const userId = req.userData.name;
const { currentName, newName } = req.body;
const filePath = path.join(req.params.filePath, req.params[0] || '');
if (!currentName || !newName) {
return res.status(400).send('Both currentName and newName must be provided.');
}
const currentPath = path.join('cdn-files', userId || '', filePath, currentName);
const newPath = path.join('cdn-files', userId, filePath, newName);
try {
await fs.promises.rename(currentPath, newPath);
const data = await fs.promises.readFile('file_info.json', 'utf8');
let fileInfo = JSON.parse(data);
let found = false;
for (let i = 0; i < fileInfo.length; i++) {
if (fileInfo[i].fileName === currentName) {
fileInfo[i].fileName = newName;
found = true;
break;
}
}
if (found) {
await fs.promises.writeFile('file_info.json', JSON.stringify(fileInfo, null, 2), 'utf8');
}
res.status(200).send('L\'opération a été effectuée avec succès.');
} catch (err) {
console.error(err);
return res.status(500).send('Erreur lors du changement de nom du fichier.');
}
});
router.post('/dashboard/delete', authMiddleware, (req, res) => {
const userId = req.userData.name;
const { filename } = req.body;
if (!userId || !filename) {
return res.status(400).json({ message: 'Identifiant d\'utilisateur ou nom de fichier manquant pour la suppression du fichier.' });
}
const userFolderPath = path.join('cdn-files', userId);
function findAndDeleteFile(folderPath) {
const filesInFolder = fs.readdirSync(folderPath);
for (const file of filesInFolder) {
const filePath = path.join(folderPath, file);
if (fs.statSync(filePath).isDirectory()) {
findAndDeleteFile(filePath);
} else if (file === filename) {
try {
fs.unlinkSync(filePath);
console.log('File deleted:', filePath);
return true;
} catch (error) {
console.error('Error deleting file:', error);
return false;
}
}
}
return false;
}
const fileDeleted = findAndDeleteFile(userFolderPath);
if (fileDeleted) {
res.status(200).json({ status: 'success', message: 'Le fichier a été supprimé avec succès.' });
} else {
res.status(404).json({ status: 'error', message: 'Le fichier que vous essayez de supprimer n\'existe pas.' });
}
});
const ncpAsync = (source, destination) => {
return new Promise((resolve, reject) => {
ncp(source, destination, (err) => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
};
router.post('/dashboard/movefile', authMiddleware, async (req, res) => {
const fileName = req.body.fileName;
const folderName = req.body.folderName;
const data = await fs.promises.readFile('user.json', 'utf8');
const users = JSON.parse(data);
const user = users.find(user => user.id === req.user.id);
if (!user) {
console.error('User not found in user.json');
return res.status(500).send('Erreur lors du déplacement du fichier.');
}
const userId = user.name;
if (!fileName || !userId) {
console.error('fileName or userId is undefined');
return res.status(500).send('Erreur lors du déplacement du fichier.');
}
const sourcePath = path.join('cdn-files', userId, fileName);
let destinationDir;
if (folderName && folderName.trim() !== '') {
destinationDir = path.join('cdn-files', userId, folderName);
} else {
destinationDir = path.join('cdn-files', userId);
}
const destinationPath = path.join(destinationDir, fileName);
try {
const normalizedSourcePath = path.normalize(sourcePath);
console.log('Full Source Path:', normalizedSourcePath);
if (fs.existsSync(normalizedSourcePath)) {
await fs.promises.access(destinationDir);
await ncpAsync(normalizedSourcePath, destinationPath);
await fs.promises.unlink(normalizedSourcePath);
} else {
console.log('File does not exist');
}
res.redirect('/dpanel/dashboard');
} catch (err) {
console.error(err);
return res.status(500).send('Erreur lors du déplacement du fichier.');
}
});
router.post('/dashboard/movefile/:folderName', authMiddleware, async (req, res) => {
const fileName = req.body.fileName;
const newFolderName = req.body.folderName;
const oldFolderName = req.params.folderName;
const userId = req.user && req.user._json ? req.userData.name : undefined;
if (!fileName || !userId || !oldFolderName || !newFolderName) {
console.error('fileName, userId, oldFolderName, or newFolderName is undefined');
return res.status(500).send('Erreur lors du déplacement du fichier.');
}
const sourcePath = path.join(process.cwd(), 'cdn-files', userId, oldFolderName, fileName);
const destinationDir = path.join(process.cwd(), 'cdn-files', userId, newFolderName);
const destinationPath = path.join(destinationDir, fileName);
try {
const normalizedSourcePath = path.normalize(sourcePath);
console.log('Full Source Path:', normalizedSourcePath);
if (fs.existsSync(normalizedSourcePath)) {
await fs.promises.access(destinationDir, fs.constants.W_OK);
await fs.promises.rename(normalizedSourcePath, destinationPath);
} else {
console.log('File does not exist');
}
res.redirect('/dpanel/dashboard');
} catch (err) {
console.error(err);
return res.status(500).send('Erreur lors du déplacement du fichier.');
}
});
router.delete('/dashboard/deletefolder/:folderName', authMiddleware, (req, res) => {
const userId = req.userData.name;
const { filename } = req.body;
const userFolderPath = path.join('cdn-files', userId || '');
const folderPath = path.join(userFolderPath, req.params.folderName || '');
if (!fs.existsSync(folderPath)) {
return res.status(404).json({ error: 'Le dossier spécifié n\'existe pas.' });
}
fs.rmdirSync(folderPath, { recursive: true });
res.json({ deleted: true, success: 'Dossier supprimé avec succès.' });
});
router.delete('/dashboard/deletefolder/:folderName', authMiddleware, (req, res) => {
const userId = req.userData.name;
const folderName = req.params.folderName;
const folderPath = path.join('cdn-files', userId, folderName);
fs.rmdir(folderPath, { recursive: true }, (err) => {
if (err) {
console.error(err);
return res.status(500).json({ error: 'Erreur lors de la suppression du dossier.' });
}
res.json({ deleted: true, success: 'Dossier supprimé avec succès.' });
});
});
router.post('/dashboard/deletefile/:folderName', authMiddleware, (req, res) => {
const userId = req.userData.name;
const { filename } = req.body;
const userFolderPath = path.join('cdn-files', userId || '');
const filePath = path.join(userFolderPath, req.params.folderName, filename || '');
if (!fs.existsSync(filePath)) {
return res.status(404).json({ error: 'Le fichier spécifié n\'existe pas.' });
}
fs.unlink(filePath, (err) => {
if (err) {
console.error(err);
return res.status(500).json({ error: 'Erreur lors de la suppression du fichier.' });
}
res.json({ deleted: true, success: 'Fichier supprimé avec succès.' });
});
});
router.get('/upload', authMiddleware, (req, res) => {
res.render('upload');
});
router.use(fileUpload({
limits: { fileSize: 15 * 1024 * 1024 * 1024 },
}));
router.post('/upload', authMiddleware, async (req, res) => {
try {
if (!req.files || Object.keys(req.files).length === 0) {
return res.status(400).send('5410 - Erreur de téléchargement, veuillez retenter ultérieurement.');
}
const file = req.files.file;
const userId = req.userData.name;
const Id = req.userData.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: 'Erreur lors du téléchargement du fichier.' });
}
const fileExtension = path.extname(originalFileName).toLowerCase();
let encryptedPassword = '';
if (password) {
const algorithm = 'aes-256-cbc';
const key = crypto.scryptSync(password, 'salt', 32);
const iv = Buffer.alloc(16, 0);
const cipher = crypto.createCipheriv(algorithm, key, iv);
encryptedPassword = cipher.update('', 'utf8', 'hex');
encryptedPassword += cipher.final('hex');
}
const fileInfo = {
fileName: originalFileName,
expiryDate: expiryDate || '',
password: encryptedPassword,
Id: Id,
path: path.join(uploadDir, originalFileName)
};
if (expiryDate || password) {
let data = [];
if (fs.existsSync('file_info.json')) {
const existingData = await fs.promises.readFile('file_info.json', 'utf8');
data = JSON.parse(existingData);
}
data.push(fileInfo);
await fs.promises.writeFile('file_info.json', JSON.stringify(data, null, 2));
}
res.redirect('/dpanel/dashboard');
});
} catch (error) {
console.error(error);
return res.status(500).send({ message: 'Erreur lors du téléchargement du fichier.' });
}
});
const User = require('../user.json');
const setup = JSON.parse(fs.readFileSync(path.join(__dirname, '../setup.json'), 'utf8'));
router.get('/dashboard/admin', authMiddleware, async (req, res) => {
try {
res.render('paramAdmin', { users: User, setup: setup });
} catch (err) {
console.error(err);
res.status(500).send('Server Error');
}
});
router.get('/dashboard/admin/users', authMiddleware, async (req, res) => {
try {
let currentPage = Number(req.query.page) || 1;
let limit = Number(req.query.limit) || 10;
let rawdata = fs.readFileSync(path.join(__dirname, '../user.json'));
let users = JSON.parse(rawdata);
let totalUsers = users.length;
let pages = Math.ceil(totalUsers / limit);
let start = (currentPage - 1) * limit;
let end = start + limit;
let usersForPage = users.slice(start, end);
res.render('paramAdminUser', { users: usersForPage, setup: setup, pages: pages, currentPage: currentPage, limit: limit });
} catch (err) {
console.error(err);
res.status(500).send('Server Error');
}
});
router.get('/dashboard/admin/settingsetup', authMiddleware, async (req, res) => {
try {
res.render('paramAdminSettingSetup', { users: User, setup: setup });
} catch (err) {
console.error(err);
res.status(500).send('Server Error');
}
});
const osUtils = require('os-utils');
const Convert = require('ansi-to-html');
const convert = new Convert();
router.get('/dashboard/admin/stats&logs', authMiddleware, async (req, res) => {
try {
const uptime = os.uptime();
const memoryUsage = process.memoryUsage().heapUsed / 1024 / 1024;
osUtils.cpuUsage(function(cpuUsage) {
fs.readdir('./logs', (err, files) => {
if (err) {
console.error(err);
res.status(500).send('Error reading logs');
return;
}
const logs = files.map(file => {
return fs.promises.readFile(path.join('./logs', file), 'utf8')
.then(content => {
content = convert.toHtml(content);
return { name: file, content: content };
})
.catch(err => {
console.error(err);
});
});
Promise.all(logs).then(completed => {
res.render('paramAdminStats&Logs', { users: User, setup: setup, uptime, memoryUsage, cpuUsage, logs: completed });
});
});
});
} catch (err) {
console.error(err);
res.status(500).send('Server Error');
}
});
router.get('/dashboard/admin/Privacy&Security', authMiddleware, async (req, res) => {
try {
const files = await fs.promises.readdir('./report');
const reports = files.filter(file => file.endsWith('.json')).map(file => {
return fs.promises.readFile(path.join('./report', file), 'utf8')
.then(content => {
return { name: file, content: content };
})
.catch(err => {
console.error(err);
});
});
Promise.all([Promise.all(reports)]).then(([completedReports]) => {
res.render('paramAdminPrivacy&Security', { users: User, reports: completedReports });
});
} catch (err) {
console.error(err);
res.status(500).send('Server Error');
}
});
router.post('/dashboard/update-role', authMiddleware, async (req, res) => {
try {
const { id, role } = req.body;
const user = User.find(user => user.id === id);
if (user) {
user.role = role;
}
fs.writeFileSync(path.join(__dirname, '../user.json'), JSON.stringify(User, null, 2));
res.redirect('/dpanel/dashboard/admin');
} catch (err) {
console.error(err);
res.status(500).send('Server Error');
}
});
router.post('/dashboard/update-setup', authMiddleware, async (req, res) => {
try {
let setup = JSON.parse(fs.readFileSync(path.join(__dirname, '../../setup.json')));
if (!req.body.ldap || !req.body.ldap.enabled) {
delete setup.ldap;
} else {
setup.ldap = req.body.ldap;
}
if (!req.body.discord || !req.body.discord.enabled) {
delete setup.discord;
} else {
setup.discord = req.body.discord;
}
setup.domain = req.body.domain;
setup.uptime = req.body.uptime;
fs.writeFileSync(path.join(__dirname, '../../setup.json'), JSON.stringify(setup, null, 2));
res.redirect('/dpanel/dashboard/admin');
} catch (err) {
console.error(err);
res.status(500).send('Server Error');
}
});
module.exports = router;

71
routes/index.js Normal file
View File

@@ -0,0 +1,71 @@
const express = require('express');
const router = express.Router();
const path = require('path');
const { checkUpdates } = require('../Middlewares/checkUpdate');
const { applyUpdate, restartCDN } = require('../models/updateManager');
const { logger, ErrorLogger, logRequestInfo } = require('../config/logs');
const util = require('util');
const exec = util.promisify(require('child_process').exec);
const authMiddleware = require('../Middlewares/authMiddleware');
const fs = require('fs');
router.get('/', (req, res) => {
res.render('acces-denied');
});
router.get('/attachments', (req, res) => {
res.render('acces-denied');
});
router.get('/checkupdate',authMiddleware, checkUpdates);
router.get('/applyupdate',authMiddleware, async (req, res) => {
const updateUrl = 'https://apollon.dinawo.fr/api/download/all';
const updateFolder = path.join(__dirname, '..');
try {
logger.info('------Before applying the update------');
await applyUpdate(updateUrl, updateFolder);
logger.info('------After applying the update------');
res.json({
success: true,
message: 'Mise à jour appliquée avec succès. Pensé à redémarrer le serveur pour que la MàJ soit prise en compte. (systemctl restart cdn).'
});
} catch (error) {
ErrorLogger.error('Error applying update:', error);
return res.status(500).json({ success: false, message: 'Erreur lors de l\'application de la mise à jour.' });
}
});
router.get('/translateAll', async (req, res) => {
const targetLanguage = req.query.lang || 'en';
const viewsFolder = path.join(__dirname, '../views');
try {
const translatedFiles = [];
const files = fs.readdirSync(viewsFolder);
for (const file of files) {
if (file.endsWith('.ejs')) {
const filePath = path.join(viewsFolder, file);
const translatedContent = await translateEJSFile(filePath, targetLanguage);
if (translatedContent !== null) {
translatedFiles.push({
fileName: file,
translatedContent,
});
}
}
}
res.json(translatedFiles);
} catch (error) {
console.error('Erreur lors de la traduction de tous les fichiers EJS :', error.message);
res.status(500).json({ error: 'Erreur de traduction' });
}
});
module.exports = router;

128
server.js Normal file
View File

@@ -0,0 +1,128 @@
const express = require('express');
const session = require('express-session');
const passport = require('passport');
const bodyParser = require('body-parser');
const { logger, logRequestInfo, ErrorLogger } = require('./config/logs');
const path = require("path");
require('dotenv').config();
const { version } = require('./package.json');
const axios = require('axios');
const app = express();
const flash = require('connect-flash');
const fs = require('fs');
const SystemReport = require('./models/reportManager.js');
let setup;
try {
setup = JSON.parse(fs.readFileSync('setup.json', 'utf8'));
} catch (err) {
console.error('Error reading setup.json:', err);
process.exit(1);
}
if (setup.discord !== undefined) {
require('./models/Passport-Discord.js');
}
if (setup.ldap !== undefined) {
require('./models/Passport-ActiveDirectory.js');
}
app.use(express.static(path.join(__dirname, 'public')));
app.get(['/user.json', '/file_info.json', '/setup.json'], (req, res) => {
res.status(403).json({ error: 'Access Denied' });
});
app.use(express.urlencoded({ extended: true }));
app.use(session({
secret: '63a69c252dfe0bb20650b6365b48dc99ad6c7eac19faed62670e73a071c54236e2faf04ee009919592def437b98d3c726c40a56ef1d8759878c1703a93244aa3',
resave: false,
saveUninitialized: true,
cookie: { secure: false }
}));app.use(passport.initialize());
app.use(passport.session());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
app.use(flash());
const indexRoute = require('./routes/index.js');
const AuthRoute = require('./routes/auth.js');
const DpanelRoute = require('./routes/dpanel.js');
const AttachmentsRoute = require('./routes/attachments.js');
app.set('view engine', 'ejs');
app.set('views', __dirname + '/views');
app.use('/', indexRoute);
app.use('/auth', AuthRoute);
app.use('/dpanel', DpanelRoute);
app.use('/attachments', AttachmentsRoute);
app.use('/public', express.static(path.join(__dirname, 'public')));
app.use(logRequestInfo);
const cron = require('node-cron');
cron.schedule('00 03 * * *', async () => {
try {
const report = await SystemReport.generate();
if (report !== null) {
logger.info('System error report generated successfully');
}
} catch (err) {
ErrorLogger.error('Error generating report :', err);
}
});
cron.schedule('0 * * * *', async () => {
try {
const fileInfoData = await fs.promises.readFile(path.join(__dirname, 'file_info.json'), 'utf8');
const fileInfo = JSON.parse(fileInfoData);
const now = new Date();
for (let index = fileInfo.length - 1; index >= 0; index--) {
const file = fileInfo[index];
const expiry = new Date(file.expiryDate);
if ((file.expiryDate && expiry <= now) || !(await fileExists(file.path))) {
fileInfo.splice(index, 1);
}
}
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}`);
}
});
async function fileExists(filePath) {
try {
await fs.promises.access(filePath);
return true;
} catch {
return false;
}
}
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.clear();
if (logger) {
logger.info(`🚀 Your server is available and running on port ${PORT}`);
logger.info(`⚜️ Application developed by Dinawo, part of the SwiftLogic Labs group`);
logger.info(`♨️ Version: ${version}`);
console.log('');
const filesData = fs.readFileSync('files.json', 'utf8');
const files = JSON.parse(filesData);
const numberOfFiles = Object.values(files).flat().length;
logger.info(`🔰 Number of files activated during server startup: ${numberOfFiles}`);
} else {
console.error('🔴 Logger is not initialized');
}
});

95
setup.py Normal file
View File

@@ -0,0 +1,95 @@
import json
import os
from getpass import getpass
setup_file_path = '/home/cdn-app/setup.json' # Specify the full path for setup.json
setup_data = {}
def print_with_color(text, color):
colors = {
'reset': '\033[0m',
'green': '\033[92m',
'yellow': '\033[93m',
'cyan': '\033[96m',
'bold': '\033[1m'
}
print(f"{colors[color]}{text}{colors['reset']}")
def create_service():
service_name = 'cdn.service'
# Create the service file
service_file_path = f'/etc/systemd/system/{service_name}'
with open(service_file_path, 'w') as service_file:
service_file.write(f'''
[Unit]
Description=CDN-APP, dinawoSR Inc
Documentation=https://cdn-app.dinawo.fr
[Service]
ExecStart=/root/.nvm/versions/node/v16.9.0/bin/node /home/cdn-app/server.js
Restart=always
User=root
Environment=PATH=/usr/bin:/usr/local/bin:/root/.nvm/versions/node/v16.9.0/bin
Environment=NODE_ENV=production
WorkingDirectory=/home/cdn-app
[Install]
WantedBy=multi-user.target
''')
# Restart systemd to acknowledge the new service
os.system('systemctl daemon-reload')
os.system('systemctl enable cdn.service')
os.system('systemctl start cdn.service')
print_with_color(f"Service {service_name} created successfully.", 'green')
# Loader
print_with_color("Welcome to server configuration script.", 'cyan')
# Change the current working directory to /home/cdn-app
os.chdir('/home/cdn-app')
# LDAP information input
setup_data['ldap'] = {
'url': input("LDAP server URL (ldap://your-ip): "),
'baseDN': input("Base DN (DC=DINAWOSRINC,DC=LAN): "),
'username': input("Username (Account Sync): "),
'password': getpass("Password (Password Account Sync): ")
}
# Prompt for domain name
setup_data['domain'] = input("Enter the domain name (cdn.domain.com): ")
# Prompt for uptime link
setup_data['uptime'] = input("Enter the uptime link (uptime.domain.com): ")
# Check Node.js version
node_version = os.popen('node -v').read().strip()
required_node_version = 'v16.9.0'
if node_version != required_node_version:
print_with_color(f"Error: Incorrect Node.js version. Required: {required_node_version}, Found: {node_version}.", 'yellow')
exit(1)
# Update npm packages with proper permissions
os.system('npm install')
# Set full permissions for /home/cdn-app
os.system('chown -R root:root /home/cdn-app')
os.system('chmod -R 777 /home/cdn-app')
# Check and modify file permissions
os.system('chmod +x /home/cdn-app/server.js')
# Save information in the setup.json file
with open(setup_file_path, 'w') as file:
json.dump(setup_data, file)
print_with_color(f"Configuration saved in the {setup_file_path} file.", 'green')
# Create the service
create_service()

149
views/AuthLogin.ejs Normal file
View File

@@ -0,0 +1,149 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@10"></script>
<link rel="stylesheet" href="/public/css/login.css">
<title>Connexion</title>
<link rel="icon" href="/public/assets/homelab_logo.png" />
</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="light-mode animate">
<div class="container mt-4 animate">
<h1 class="title text-center animate">Connexion</h1>
<% if (currentUrl === '/auth/activedirectory' || setupData.hasOwnProperty('ldap')) { %>
<h3 class="text-center animate">Connexion avec Active Directory</h3>
<form action="/auth/activedirectory" method="post" class="mb-4 animate">
<% if (typeof errorMessage !== 'undefined' && errorMessage) { %>
<% } %>
<div class="form-group animate">
<label for="username" class="animate">Nom d'utilisateur :</label>
<input type="text" id="username" name="username" class="form-control animate" required>
</div>
<div class="form-group animate">
<label for="password" class="animate">Mot de passe :</label>
<input type="password" id="password" name="password" class="form-control animate" required>
</div>
<button type="submit" class="btn btn-primary d-flex align-items-center justify-content-center mx-auto d-block custom-btn">
<span class="ml-2 animate">Se connecter</span>
</button>
</form>
<% } %>
<% if (typeof setupData.discord !== 'undefined') { %>
<% if (typeof setupData.ldap !== 'undefined') { %>
<h3 class="text-center animate">Ou</h3>
<% } %>
<button onclick="location.href='/auth/discord'" class="btn btn-primary d-flex align-items-center justify-content-center mx-auto d-block custom-btn">
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="#ffffff" width="24" height="24">
<title>Discord</title>
<path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z"/>
</svg>
<span class="ml-2 animate">Se connecter avec Discord</span>
</button>
<% } %>
<br>
<div class="d-flex justify-content-center animate">
<button id="themeSwitcher" class="btn btn-warning mt-3 animate ">Changer de Thème</button>
</div>
</div>
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"></script>
<script>
const body = document.body;
document.getElementById('themeSwitcher').addEventListener('click', function () {
body.classList.toggle('dark-mode');
});
document.addEventListener('DOMContentLoaded', function () {
const darkModeSwitch = document.getElementById('darkModeSwitch');
const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
body.classList.toggle('dark-mode', darkModeMediaQuery.matches);
darkModeMediaQuery.addListener(function (e) {
body.classList.toggle('dark-mode', e.matches);
});
});
var isAuthenticated = "<%= isAuthenticated %>" === "true";
var errorMessage = '<%= errorMessage %>';
if (isAuthenticated) {
Swal.fire({
position: 'top',
icon: 'success',
title: 'Authentification réussie!',
text: 'Vous allez être redirigé vers le tableau de bord.',
showConfirmButton: false,
timer: 1800,
timerProgressBar: true,
toast: true,
willClose: () => {
window.location.replace('/dpanel/dashboard');
}
});
} else if (errorMessage) {
Swal.fire({
position: 'top',
icon: 'error',
title: 'Erreur lors de l\'authentification!',
text: 'Merci de reessayer.',
showConfirmButton: false,
timer: 2200,
timerProgressBar: true,
toast: true,
});
}
</script>
</body>
</html>

92
views/acces-denied.ejs Normal file
View File

@@ -0,0 +1,92 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" href="/public/assets/homelab_logo.png"/>
<title>Temporary Network Error</title>
<style>
body {
background-color: #313338;
color: #ffffff;
font-family: Arial, sans-serif;
text-align: center;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
height: 100vh;
}
.logo {
width : 150px;
height : 150px;
}
.error-message {
font-size: 24px;
margin-top: 50px;
}
.request-id {
font-size: 18px;
color: #ffffff;
margin-top: 45px;
}
.promotion {
margin-top: 30px;
font-size: 18px;
color: #ffffff;
}
.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;
}
</style>
</head>
<body>
<img class="logo" id="logo" src="../public/assets/homelab_logo.png">
<div class="error-message">
Temporary Network Error<br>
You do not have permission to access the requested resource, or there is an error. Please try again later.
</div>
<div class="request-id">
Request ID: <span id="request-id"></span>
</div>
<script>
document.getElementById('request-id').textContent = Math.random().toString(16).slice(2);
</script>
<br><a class="custom-btn" href="#" target="_blank">Check out the Uptime Page</a>
<div class="promotion">
<p>The modern CDN for secure file transfer</p>
<p>Power your project with our self-hosted CDN</p>
<p>Experience exceptional performance and ultra-fast content distribution.</p><br>
<a class="custom-btn" href="https://cdn-app.dinawo.fr" target="_blank">Learn more about our CDN</a><br>
<p>Created by SwiftLogic Labs.</p>
<a class="custom-btn" href="https://discord.gg/k6r96Tmtgx" target="_blank">Join our Discord Server</a>
</div>
</body>
</html>

251
views/dashboard.ejs Normal file
View File

@@ -0,0 +1,251 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"
integrity="sha384-GLhlTQ8iRABdZLl6O5oVMWSktQOp6b7In1Zl3/JiR3eZB1+nHN/8u8UqXj2l1tji" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
<script src="https://code.jquery.com/jquery-3.6.4.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script src="https://cdn.jsdelivr.net/npm/toastify-js"></script>
<script src="/public/js/dashboard.js"></script>
<link rel="stylesheet" href="/public/css/styles.css" />
<title>Dashboard</title>
<link rel="icon" href="/public/assets/homelab_logo.png" />
<link rel="apple-touch-icon" sizes="57x57" href="/public/assets/homelab_logo@2x.png">
<link rel="apple-touch-icon" sizes="60x60" href="/public/assets/homelab_logo@3x.png">
</head>
<body class="light-mode">
<nav class="navbar navbar-expand-md navbar-light bg-light header">
<a class="navbar-brand">
Dashboard CDN
<span class="badge badge-info ml-1">Beta</span>
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav"
aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ml-auto">
<li class="nav-item">
<form action="/dpanel/upload" class="form-inline">
<button class="btn btn-primary btn-round mr-2">
<i class="fas fa-cloud-upload-alt"></i> Téléverser un fichier
</button>
</form>
</li>
<li class="nav-item">
<form id="newFolderModalForm" class="form-inline ml-2">
<button type="submit" class="btn btn-success btn-round ml-2" id="newFolderModalBtn" data-toggle="modal" data-target="#newFolderModal">
<i class="fas fa-folder-open"></i> Nouveau Dossier
</button>
</form>
</li>
<li class="nav-item">
<button id="styleSwitcher" class="btn btn-link btn-round">
<i id="themeIcon"></i>
</button>
</li>
<li class="nav-item dropdown">
<a class="btn dropdown-toggle" id="accountDropdownBtn" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<img src="https://api.dicebear.com/7.x/initials/svg?seed=<%= user.name %>" alt="User Icon" class="rounded-circle" style="width: 30px; height: 30px;" />
</a>
<div class="dropdown-menu dropdown-menu-right p-4 bg-dark text-white" id="accountDropdownMenu">
<div class="mb-3 text-center">
<h6 class="text-lg font-semibold"><%= user.name %></h6>
<p class="mb-0">ID: <%= user.id %></p>
<p class="mb-0">Role: <%= user.role %></p>
</div>
<div class="dropdown-divider mb-2"></div>
<% if (user.role === 'admin') { %>
<a class="dropdown-item text-center text-white no-hover custom-btn" href="/dpanel/dashboard/admin" id="adminLink">
<i class="fas fa-user-shield"></i> Administration du site
</a>
<% } %>
<a class="dropdown-item text-center text-white no-hover" href="/auth/logout" id="logoutLink">
<i class="fas fa-sign-out-alt text-white"></i> Déconnexion
</a>
</div>
</li>
</ul>
</div>
</nav>
<div class="container mt-4 table-container">
<div class="table-responsive">
<table class="table w-100 table table-hover">
<thead>
<tr>
<th>Nom du fichier</th>
<th>Taille</th>
<th class="text-right">Action</th>
</tr>
</thead>
<tbody>
<% files.forEach(file => { %>
<tr data-extension="<%= file.extension %>" data-type="<%= file.type %>">
<% if (fileInfoNames.includes(file.name)) { %>
<td><a href="#" onclick="showFileInfo('<%= file.name %>')"><%= file.name %></a></td>
<% } else { %>
<td><%= file.name %></td>
<% } %> <td>
<% if (file.type === 'folder') { %>
Dossier
<% } else { %>
<%
const fileSizeInBytes = file.size;
let fileSize;
if (fileSizeInBytes < 1024 * 1024) {
fileSize = `${(fileSizeInBytes / 1024).toFixed(2)} Ko`;
} else if (fileSizeInBytes < 1024 * 1024 * 1024) {
fileSize = `${(fileSizeInBytes / (1024 * 1024)).toFixed(2)} Mo`;
} else {
fileSize = `${(fileSizeInBytes / (1024 * 1024 * 1024)).toFixed(2)} Go`;
}
%>
<%= fileSize %>
<% } %>
</td>
<td class="d-flex justify-content-end align-items-center">
<% if (file.type === 'folder') { %>
<form class="file-actions mb-2">
<input type="hidden" name="folderName" value="<%= file.name %>">
<button class="delete-folder-button btn btn-danger btn-round" type="button">
<i class="fas fa-trash-alt"></i>
</button>
</form>
<a href="/dpanel/dashboard/folder/<%= file.name %>" class="btn btn-primary btn-round mb-2">
<i class="fas fa-folder-open"></i> Accéder
</a>
<% } else { %>
<button class="btn btn-primary btn-round" onclick="renameFile('<%= folderName %>', '<%= file.name %>')">
<i class="fas fa-edit"></i> Renommer
</button>
<form class="file-actions mb-2" id="deleteForm" action="/dpanel/dashboard/delete" method="post">
<input type="hidden" name="_method" value="DELETE">
<input type="hidden" name="filename" value="<%= file.name %>">
<button class="delete-button btn btn-danger btn-round" type="button" onclick="confirmDelete('<%= file.name %>')">
<i class="fas fa-trash-alt"></i>
</button>
</form>
<form class="file-actions mb-2">
<div class="copy-link-container d-flex align-items-center">
<input type="text" class="file-link form-control rounded mr-2" value="<%= file.url %>" readonly style="display: none;">
<button class="button copy-button btn btn-success btn-round" data-file="<%= file.name %>">
<i class="fas fa-copy"></i>
</button>
</div>
</form>
<form class="file-actions d-flex align-items-center mb-2" action="/dpanel/dashboard/movefile" method="post">
<input type="hidden" name="fileName" value="<%= file.name %>">
<select class="form-control rounded mr-2" name="folderName">
<option value="" disabled selected>Déplacer vers...</option>
<% allFolders.forEach(folder => { %>
<option value="<%= folder %>"><%= folder %></option>
<% }); %>
</select>
<button type="submit" class="btn btn-success btn-round">Déplacer</button>
</form>
<% } %>
</td>
</tr>
<% }); %>
</tbody>
</table>
</div>
</div>
<div class="modal fade" id="patchNoteModal" tabindex="-1" role="dialog" aria-labelledby="patchNoteModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg rounded-lg" role="document">
<div class="modal-content dark-mode">
<div class="modal-header">
<h5 class="modal-title" id="patchNoteModalLabel">Patch Note<span class="badge badge-info ml-1">v1.0.9-beta</span></h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="patch-note-item">
<p><i class="fas fa-wrench"></i> Problèmes Résolus :</p>
<ul>
<li>Correction d'un Problème d'Authentification :</li>
<ul>
<li>Résolution d'un problème où l'authentification échouait pour les utilisateurs se connectant pour la première fois et n'étant pas présents dans user.json.<span class="badge badge-warning ml-1">PATCH</span></li>
<li>Correction du problème d'authentification avec Active Directory (AD) entraînant des échecs d'authentification.<span class="badge badge-warning ml-1">PATCH</span></li>
</ul>
<li>Correction du Problème de Compression des Journaux :</li>
<ul>
<li>Correction du problème des journaux compressés, assurant ainsi que les rapports et les affichages dans le panneau d'administration fonctionnent désormais comme prévu.<span class="badge badge-warning ml-1">PATCH</span></li>
</ul>
</ul>
</div>
<div class="patch-note-item">
<p><i class="fas fa-wrench"></i> Améliorations Générales :</p>
<ul>
<li>Corrections Mineures :</li>
<ul>
<li>Mise en place de diverses corrections mineures pour améliorer la stabilité générale et la fonctionnalité du système.<span class="badge badge-warning ml-1">PATCH</span></li>
<li>Résolution de petits problèmes signalés par la communauté des utilisateurs pour offrir une expérience plus fluide.<span class="badge badge-warning ml-1">PATCH</span></li>
</ul>
<li>Optimisation des Performances :</li>
<ul>
<li>Amélioration des performances du système, résultant en des temps de réponse plus rapides et une efficacité accrue à tous les niveaux.<span class="badge badge-success ml-1">AJOUT</span></li>
</ul>
</ul>
</div>
<div class="patch-note-item">
<p><i class="fas fa-shield-alt"></i> Sécurité :</p>
<ul>
<li>Correction Critique de Sécurité - Gérée par SwiftLogic CyberShield :</li>
<ul>
<li>Mise en place d'une correction critique de sécurité pour remédier aux vulnérabilités potentielles et assurer un environnement plus sûr pour les utilisateurs.<span class="badge badge-warning ml-1">PATCH</span></li>
</ul>
</ul>
</div>
<div class="patch-note-item">
<p>Remarque : Merci de continuer à fournir vos retours d'expérience et rapports de bugs pour nous aider à améliorer constamment notre plateforme. Nous sommes reconnaissants de votre engagement et de votre contribution.</p>
</div>
</div>
</div>
</div>
</div>
<div class="container">
<footer class="py-3 my-4">
<ul class="nav justify-content-center border-bottom pb-3 mb-3">
<li class="nav-item">
<a class="nav-link px-2 text-muted" href="#" data-toggle="modal" data-target="#patchNoteModal">
Version: 1.0.9-beta
</a>
</li>
</ul>
<div class="d-flex justify-content-center align-items-center">
<p class="text-center text-muted mb-0">&copy; 2024 SwiftLogic Labs</p>
<div class="ml-3">
<% if (user.role === 'admin') { %>
<button id="checkUpdateButton" class="btn btn-icon btn-info btn-round" title="Vérifier les mises à jour" onclick="checkUpdates()">
<div class="d-flex align-items-center">
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" viewBox="0 0 512 512">
<path d="M370.7 133.3C339.5 104 298.9 88 255.8 88c-77.5 .1-144.3 53.2-162.8 126.9-1.3 5.4-6.1 9.2-11.7 9.2H24.1c-7.5 0-13.2-6.8-11.8-14.2C33.9 94.9 134.8 8 256 8c66.4 0 126.8 26.1 171.3 68.7L463 41C478.1 25.9 504 36.6 504 57.9V192c0 13.3-10.7 24-24 24H345.9c-21.4 0-32.1-25.9-17-41l41.8-41.7zM32 296h134.1c21.4 0 32.1 25.9 17 41l-41.8 41.8c31.3 29.3 71.8 45.3 114.9 45.3 77.4-.1 144.3-53.1 162.8-126.8 1.3-5.4 6.1-9.2 11.7-9.2h57.3c7.5 0 13.2 6.8 11.8 14.2C478.1 417.1 377.2 504 256 504c-66.4 0-126.8-26.1-171.3-68.7L49 471C33.9 486.1 8 475.4 8 454.1V320c0-13.3 10.7-24 24-24z"/>
</svg>
<span class="ml-2 d-none">Vérifier les mises à jours</span>
</div>
</button>
<% } %>
</div>
</div>
</footer>
</div>
</body>
</html>

View File

@@ -0,0 +1,47 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Fichier non trouvé</title>
<style>
body {
font-family: Arial, sans-serif;
background-image: linear-gradient(to right, #ff5a5e, #f93838);
color: #fff;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
}
.container {
text-align: center;
}
h1 {
font-size: 2rem;
margin-bottom: 1rem;
}
p {
font-size: 1.2rem;
}
.error {
font-size: 1.5rem;
color: darkred;
margin-top: 2rem;
}
</style>
</head>
<body>
<div class="container">
<h1>Erreur lors de la récupération des fichiers</h1>
<p class="error">Code d'erreur: U5nV4l1dUs3r_FileRetrievalError</p>
</div>
</body>
</html>
</body>
</html>

49
views/file-expired.ejs Normal file
View File

@@ -0,0 +1,49 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Fichier non trouvé</title>
<link rel="icon" href="/public/assets/homelab_logo.png" />
<style>
body {
font-family: Arial, sans-serif;
background-image: linear-gradient(to right, #66eaa8, #a2674b);
color: #fff;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
}
.container {
text-align: center;
}
h1 {
font-size: 2rem;
margin-bottom: 1rem;
}
p {
font-size: 1.2rem;
}
.error {
font-size: 1.5rem;
color: darkred;
margin-top: 2rem;
}
</style>
</head>
<body>
<div class="container">
<h1>Désolé, ce fichier a expiré 😔</h1>
<p>Le fichier que vous essayez d'accéder a expiré et n'est plus disponible.</p>
<p class="error">Code d'erreur: Ysx457S_FileExpired</p>
</div>
</body>
</html>
</body>
</html>

48
views/file-not-found.ejs Normal file
View File

@@ -0,0 +1,48 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Fichier non trouvé</title>
<style>
body {
font-family: Arial, sans-serif;
background-image: linear-gradient(to right, #667eea, #764ba2);
color: #fff;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
}
.container {
text-align: center;
}
h1 {
font-size: 2rem;
margin-bottom: 1rem;
}
p {
font-size: 1.2rem;
}
.error {
font-size: 1.5rem;
color: darkred;
margin-top: 2rem;
}
</style>
</head>
<body>
<div class="container">
<h1>Fichier non trouvé</h1>
<p>Désolé, le fichier que vous recherchez est introuvable.</p>
<p class="error">Code d'erreur: XyZ789_FileNotFound</p>
</div>
</body>
</html>
</body>
</html>

181
views/folder.ejs Normal file
View File

@@ -0,0 +1,181 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"
integrity="sha384-GLhlTQ8iRABdZLl6O5oVMWSktQOp6b7In1Zl3/JiR3eZB1+nHN/8u8UqXj2l1tji" crossorigin="anonymous">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
<script src="https://code.jquery.com/jquery-3.6.4.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css">
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script src="https://cdn.jsdelivr.net/npm/toastify-js"></script>
<link rel="stylesheet" href="/public/css/styles.css" />
<script src="/public/js/folder.js"></script>
<title>Dashboard</title>
<link rel="icon" href="/public/assets/homelab_logo.png" />
</head>
<body class="light-mode">
<nav class="navbar navbar-expand-lg navbar-light bg-light header">
<a class="navbar-brand">
Dashboard CDN
<span class="badge badge-info ml-1">Beta</span>
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav"
aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse ml-auto" id="navbarNav">
<ul class="navbar-nav ml-auto">
<li class="nav-item">
<button class="btn btn-warning btn-round mr-2" onclick="window.location.href='/dpanel/dashboard';">
<i class="fas fa-home"></i>Page principal</button>
</li>
<li class="nav-item">
<form action="/dpanel/upload" class="form-inline">
<button class="btn btn-primary btn-round mr-2">
<i class="fas fa-cloud-upload-alt"></i> Téléverser un fichier
</button>
</form>
</li>
<li class="nav-item">
<button id="styleSwitcher" class="btn btn-link btn-round">
<span id="themeIcon" class="fas theme-icon"></span>
</button>
</li>
</ul>
</div>
</nav>
<nav aria-label="breadcrumb">
<ol class="breadcrumb custom-breadcrumb">
<li class="breadcrumb-item"><a href="/dpanel/dashboard">Accueil</a></li>
<% let pathSegments = currentFolder.split('/'); %>
<% pathSegments.forEach((segment, index) => { %>
<% let pathSoFar = pathSegments.slice(0, index + 1).join('/'); %>
<li class="breadcrumb-item <%= (index === pathSegments.length - 1) ? 'active' : '' %>">
<% if (index === pathSegments.length - 1) { %>
<%= segment %>
<% } else { %>
<a href="/dpanel/dashboard/folder/<%= pathSoFar %>"><%= segment %></a>
<% } %>
</li>
<% }); %>
</ol>
</nav>
</body>
<% function formatSize(sizeInBytes) {
if (sizeInBytes < 1024) {
return `${sizeInBytes} octets`;
} else if (sizeInBytes < 1024 * 1024) {
return `${(sizeInBytes / 1024).toFixed(2)} Ko`;
} else if (sizeInBytes < 1024 * 1024 * 1024) {
return `${(sizeInBytes / (1024 * 1024)).toFixed(2)} Mo`;
} else {
return `${(sizeInBytes / (1024 * 1024 * 1024)).toFixed(2)} Go`;
}
}
%>
<div class="container mt-4 table-container">
<div class="table-responsive">
<table class="table w-100">
<thead>
<tr>
<th>Nom du fichier</th>
<th>Taille</th>
<th class="text-right">Action</th>
</tr>
</thead>
<tbody>
<% files.forEach(file => { %>
<tr data-extension="<%= file.extension %>" data-type="<%= file.type %>">
<% if (fileInfoNames.includes(file.name)) { %>
<td><a href="#" onclick="showFileInfo('<%= file.name %>')"><%= file.name %></a></td>
<% } else { %>
<td><%= file.name %></td>
<% } %>
<td>
<% if (file.type === 'folder') { %>
<% const folderSize = calculateFolderSize(file.contents); %>
<%= (folderSize !== undefined && !isNaN(folderSize)) ? formatSize(folderSize) : 'Taille inconnue' %>
<% } else { %>
<%
const fileSizeInBytes = file.size;
let fileSize;
if (fileSizeInBytes !== undefined && !isNaN(fileSizeInBytes) && fileSizeInBytes >= 0) {
fileSize = formatSize(fileSizeInBytes);
} else {
console.error('Invalid file size:', fileSizeInBytes);
fileSize = 'Taille inconnue';
}
%>
<%= fileSize %>
<% } %>
</td>
<td class="d-flex justify-content-end align-items-center">
<% if (file.type === 'folder') { %>
<a href="/dpanel/dashboard/folder/<%= file.name %>" class="btn btn-primary btn-round mb-2">
<i class="fas fa-folder-open fa-xs btn-icon"></i> Accéder
</a>
<% } else { %>
<button class="btn btn-primary btn-round" onclick="renameFile('<%= folderName %>', '<%= file.name %>')">
<i class="fas fa-edit fa-xs btn-icon"></i> Renommer
</button>
<form class="file-actions mb-2" id="deleteForm" action="/dpanel/dashboard/delete" method="post">
<input type="hidden" name="_method" value="DELETE">
<input type="hidden" name="filename" value="<%= file.name %>">
<button class="delete-button btn btn-danger btn-round" type="button" onclick="confirmDeleteFile('<%= currentFolder %>', '<%= file.name %>')">
<i class="fas fa-trash-alt fa-xs btn-icon"></i>
</button>
</form>
<form class="file-actions mb-2">
<div class="copy-link-container d-flex align-items-center">
<input type="text" class="file-link form-control rounded mr-2" value="<%= file.url %>" readonly style="display: none;">
<button class="button copy-button btn btn-success btn-round" data-file="<%= file.name %>">
<i class="fas fa-copy fa-xs btn-icon"></i>
</button>
</div>
</form>
<form class="file-actions d-flex align-items-center mb-2" action="/dpanel/dashboard/movefile/<%= folderName %>" method="post">
<input type="hidden" name="fileName" value="<%= file.name %>">
<select class="form-control rounded mr-2" name="folderName">
<option value="" disabled selected>Déplacer vers...</option>
<% allFolders.forEach(folder => { %>
<option value="<%= folder %>"><%= folder %></option>
<% }); %>
</select>
<button type="submit" class="btn btn-success btn-round">Déplacer</button>
</form>
<% } %>
</td>
</tr>
<% }); %>
</tbody>
</table>
</div>
</div>
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"></script>
<script>
</script>
<div class="container">
<footer class="py-3 my-4">
<ul class="nav justify-content-center border-bottom pb-3 mb-3">
<li class="nav-item"><a class="nav-link px-2 text-muted">Version: 1.0.9-beta</a></li>
</ul>
<p class="text-center text-muted">&copy; 2024 SwiftLogic Labs</p>
</footer>
</div>
</body>
</html>

107
views/paramAdmin.ejs Normal file
View File

@@ -0,0 +1,107 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css">
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@10"></script>
<link rel="stylesheet" href="/public/css/login.css">
<title>Parameter Admin</title>
<link rel="icon" href="/public/assets/homelab_logo.png" />
</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;
width: 50%;
margin: 0 auto;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.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="light-mode animate">
<div class="container mt-4 animate">
<h1 class="title text-center animate">Bienvenue dans les parametres admin</h1>
<h4 class="title text-center animate">Que souhaitez vous faire ?</h4><br>
<a class="dropdown-item text-center text-white no-hover custom-btn" href="/dpanel/dashboard/admin/users?page=1&limit=10" id="adminLink1">
<i class="fas fa-users"></i> Gérer les utilisateurs
</a><br>
<a class="dropdown-item text-center text-white no-hover custom-btn" href="/dpanel/dashboard/admin/settingsetup" id="adminLink2">
<i class="fas fa-cogs"></i> Modifier les parametres de configuration
</a><br>
<a class="dropdown-item text-center text-white no-hover custom-btn" href="/dpanel/dashboard/admin/stats&logs" id="adminLink3">
<i class="fas fa-chart-bar"></i> Afficher les statistiques & les logs
</a><br>
<a class="dropdown-item text-center text-white no-hover custom-btn" href="/dpanel/dashboard/admin/Privacy&Security" id="adminLink4">
<i class="fas fa-shield-alt"></i> Confidentialité & Sécurité
</a><br><br>
<a class="dropdown-item text-center text-white no-hover custom-btn" href="/dpanel/dashboard/" id="adminLink5">
<i class="fas fa-home"></i> Retourner au dashboard
</a>
<div class="d-flex justify-content-center animate">
<button id="themeSwitcher" class="btn btn-warning mt-3 animate ">Changer de Thème</button>
</div>
</div>
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"></script>
<script>
const body = document.body;
document.getElementById('themeSwitcher').addEventListener('click', function () {
body.classList.toggle('dark-mode');
});
document.addEventListener('DOMContentLoaded', function () {
const darkModeSwitch = document.getElementById('darkModeSwitch');
const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
body.classList.toggle('dark-mode', darkModeMediaQuery.matches);
darkModeMediaQuery.addListener(function (e) {
body.classList.toggle('dark-mode', e.matches);
});
});
</script>
</body>
</html>

View File

@@ -0,0 +1,200 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@10"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<link rel="stylesheet" href="/public/css/login.css">
<title>Confidentialité et Sécurité</title>
<link rel="icon" href="/public/assets/homelab_logo.png" />
<style>
.modal {
display: none; /* Hidden by default */
position: fixed; /* Stay in place */
z-index: 1; /* Sit on top */
padding-top: 100px; /* Location of the box */
left: 0;
top: 0;
width: 100%; /* Full width */
height: 100%; /* Full height */
overflow: auto; /* Enable scroll if needed */
background-color: rgb(0,0,0); /* Fallback color */
background-color: rgba(0,0,0,0.4); /* Black w/ opacity */
}
/* Modal Content */
.modal-content {
background-color: #fefefe;
margin: auto;
padding: 20px;
border: 1px solid #888;
width: 80%;
}
/* The Close Button */
.close {
color: #aaaaaa;
float: right;
font-size: 28px;
font-weight: bold;
}
.close:hover,
.close:focus {
color: #000;
text-decoration: none;
cursor: pointer;
}
.custom-btn {
transition: transform 0.3s ease, background-color 0.3s ease, border-color 0.3s ease;
color: #007BFF;
background-color: transparent;
padding: 5px 10px;
text-decoration: none;
display: inline-block;
font-size: 14px;
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;
}
</style>
</head>
<body class="light-mode animate">
<div class="container mt-4 table-container animate">
<h2 class="text-center">Confidentialité et Sécurité</h2><br>
<h2>Données d'analyse</h2>
<div class="row">
<% reports && reports.forEach((report, index) => { %>
<% if (report) { %>
<div class="col-md-4 text-center">
<button class="btn btn-primary custom-btn reportName" data-index="<%= index %>"><%= report.name %></button>
</div>
<% if ((index + 1) % 3 === 0) { %>
</div><div class="row">
<% } %>
<div id="myReportModal<%= index %>" class="modal">
<div class="modal-content">
<span class="close" data-index="<%= index %>">&times;</span>
<pre><%= report.content %></pre>
</div>
</div>
<% } %>
<% }); %>
</div>
<br><div class="d-flex justify-content-center align-items-center">
<a class="dropdown-item text-center text-white no-hover custom-btn" href="/dpanel/dashboard/admin/" style="max-width: 250px; padding: 10px;">
<i class="fas fa-sign-out-alt text-white "></i> Retourner au dashboard admin
</a>
</div>
<div class="d-flex justify-content-center animate">
<button id="themeSwitcher" class="btn btn-warning mt-3 animate ">Changer de Thème</button>
</div>
<script>
var logNames = document.getElementsByClassName("logName");
var closeButtons = document.getElementsByClassName("close");
// Add event listeners to all log names
for (var i = 0; i < logNames.length; i++) {
logNames[i].addEventListener("click", function(event) {
var index = event.target.getAttribute("data-index");
var modal = document.getElementById("myModal" + index);
modal.style.display = "block";
});
}
// Add event listeners to all close buttons
for (var i = 0; i < closeButtons.length; i++) {
closeButtons[i].addEventListener("click", function(event) {
var index = event.target.getAttribute("data-index");
var modal = document.getElementById("myModal" + index);
modal.style.display = "none";
});
}
// When the user clicks anywhere outside of the modal, close it
window.onclick = function(event) {
if (event.target.className === "modal") {
event.target.style.display = "none";
}
}
var reportNames = document.getElementsByClassName("reportName");
var reportCloseButtons = document.getElementsByClassName("close");
// Add event listeners to all report names
for (var i = 0; i < reportNames.length; i++) {
reportNames[i].addEventListener("click", function(event) {
var index = event.target.getAttribute("data-index");
var modal = document.getElementById("myReportModal" + index);
modal.style.display = "block";
});
}
// Add event listeners to all close buttons
for (var i = 0; i < reportCloseButtons.length; i++) {
reportCloseButtons[i].addEventListener("click", function(event) {
var index = event.target.getAttribute("data-index");
var modal = document.getElementById("myReportModal" + index);
modal.style.display = "none";
});
}
// When the user clicks anywhere outside of the modal, close it
window.onclick = function(event) {
if (event.target.className === "modal") {
event.target.style.display = "none";
}
}
const body = document.body;
document.getElementById('themeSwitcher').addEventListener('click', function () {
if (body.classList.contains('dark-mode')) {
body.classList.remove('dark-mode');
body.classList.add('light-mode');
} else {
body.classList.remove('light-mode');
body.classList.add('dark-mode');
}
});
document.addEventListener('DOMContentLoaded', function () {
const darkModeSwitch = document.getElementById('darkModeSwitch');
const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
body.classList.toggle('dark-mode', darkModeMediaQuery.matches);
darkModeMediaQuery.addListener(function (e) {
body.classList.toggle('dark-mode', e.matches);
});
});
</script>
</body>
</script>
</div>
</body>
</html>

View File

@@ -0,0 +1,254 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@10"></script>
<link rel="stylesheet" href="/public/css/login.css">
<title>Parameter Admin</title>
<link rel="icon" href="/public/assets/homelab_logo.png" />
<style>
.custom-btn {
transition: transform 0.3s ease, background-color 0.3s ease, border-color 0.3s ease;
color: #007BFF;
background-color: transparent;
padding: 5px 10px;
text-decoration: none;
display: inline-block;
font-size: 14px;
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;
}
input[type="text"] {
width: 100%;
padding: 5px 15px;
margin: 8px 0;
box-sizing: border-box;
border: none;
border-bottom: 2px solid #007BFF;
border-radius: 4px;
}
input[type="text"]:focus {
border-bottom: 2px solid #555;
}
.switch {
position: relative;
display: inline-block;
width: 60px;
height: 34px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
-webkit-transition: .4s;
transition: .4s;
}
.slider:before {
position: absolute;
content: "";
height: 26px;
width: 26px;
left: 4px;
bottom: 4px;
background-color: white;
-webkit-transition: .4s;
transition: .4s;
}
input:checked + .slider {
background-color: #2196F3;
}
input:focus + .slider {
box-shadow: 0 0 1px #2196F3;
}
input:checked + .slider:before {
-webkit-transform: translateX(26px);
-ms-transform: translateX(26px);
transform: translateX(26px);
}
/* Rounded sliders */
.slider.round {
border-radius: 34px;
}
.slider.round:before {
border-radius: 50%;
}
</style>
</head>
<body class="light-mode animate">
<div class="container mt-4 table-container animate">
<h2 class="text-center">Gestion de la configuration</h2><br>
<div class="table-responsive">
<form action="/dpanel/dashboard/update-setup" method="POST">
<table class="table w-100">
<thead>
<tr>
<th>Paramètre</th>
<th>Valeur</th>
</tr>
</thead>
<tbody>
<tr>
<td colspan="2"><h2>LDAP Settings</h2></td>
</tr>
<tr>
<td>Activer LDAP:</td>
<td>
<label class="switch">
<input type="checkbox" name="ldap[enabled]" <%= setup.ldap ? 'checked' : '' %>
onchange="toggleForm('ldapForm', this)">
<span class="slider round"></span>
</label>
</td>
</tr>
<tbody id="ldapForm" style="display: block">
<tr>
<td>URL:</td>
<td><input type="text" name="ldap[url]" value="<%= setup.ldap ? setup.ldap.url : '' %>"></td>
</tr>
<tr>
<td>Base DN:</td>
<td><input type="text" name="ldap[baseDN]" value="<%= setup.ldap ? setup.ldap.baseDN : '' %>"></td>
</tr>
<tr>
<td>Username:</td>
<td><input type="text" name="ldap[username]" value="<%= setup.ldap ? setup.ldap.username : '' %>"></td>
</tr>
<tr>
<td>Password:</td>
<td><input type="text" name="ldap[password]" value="<%= setup.ldap ? setup.ldap.password : '' %>"></td>
</tr>
</tbody>
<tr>
<td colspan="2"><h2>Discord Settings</h2></td>
</tr>
<tr>
<td>Activer Discord:</td>
<td>
<label class="switch">
<input type="checkbox" name="discord[enabled]" <%= setup.discord ? 'checked' : '' %>
onchange="toggleForm('discordForm', this)">
<span class="slider round"></span>
</label>
</td>
</tr>
<tbody id="discordForm" style="display: block">
<tr>
<td>Client ID:</td>
<td><input type="text" name="discord[clientID]" value="<%= setup.discord ? setup.discord.clientID : '' %>"></td>
</tr>
<tr>
<td>Client Secret:</td>
<td><input type="text" name="discord[clientSecret]" value="<%= setup.discord ? setup.discord.clientSecret : '' %>"></td>
</tr>
<tr>
<td>Identify URL:</td>
<td><input type="text" name="discord[identifyURL]" value="<%= setup.discord ? setup.discord.identifyURL : '' %>"></td>
</tr>
<tr>
<td>Authorized IDs:</td>
<td><input type="text" name="discord[authorizedIDs]" value="<%= setup.discord ? setup.discord.authorizedIDs : '' %>">
</td>
</tr>
</tbody>
<tr>
<td colspan="2"><h2>Other Settings</h2></td>
</tr>
<tr>
<td>Domain:</td>
<td><input type="text" name="domain" value="<%= setup.domain %>"></td>
</tr>
<tr>
<td>Uptime:</td>
<td><input type="text" name="uptime" value="<%= setup.uptime %>"></td>
</tr>
</tbody>
</table>
<div class="text-center">
<button type="submit" class="btn btn-primary custom-btn">Mettre à jour</button>
</div>
</form>
</div>
<div class="d-flex justify-content-center align-items-center">
<a class="dropdown-item text-center text-white no-hover custom-btn" href="/dpanel/dashboard/admin/" style="max-width: 250px; padding: 10px;">
<i class="fas fa-sign-out-alt text-white "></i> Retourner au dashboard admin
</a>
</div>
<div class="d-flex justify-content-center animate">
<button id="themeSwitcher" class="btn btn-warning mt-3 animate ">Changer de Thème</button>
</div>
</div>
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"></script>
<script>
const body = document.body;
document.getElementById('themeSwitcher').addEventListener('click', function () {
if (body.classList.contains('dark-mode')) {
body.classList.remove('dark-mode');
body.classList.add('light-mode');
} else {
body.classList.remove('light-mode');
body.classList.add('dark-mode');
}
});
document.addEventListener('DOMContentLoaded', function () {
const darkModeSwitch = document.getElementById('darkModeSwitch');
const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
body.classList.toggle('dark-mode', darkModeMediaQuery.matches);
darkModeMediaQuery.addListener(function (e) {
body.classList.toggle('dark-mode', e.matches);
});
});
function toggleForm(formId, checkbox) {
const form = document.getElementById(formId);
form.style.display = checkbox.checked ? 'block' : 'none';
}
</script>
</body>
</html>

View File

@@ -0,0 +1,192 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@10"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<link rel="stylesheet" href="/public/css/login.css">
<title>Statistiques du serveur</title>
<link rel="icon" href="/public/assets/homelab_logo.png" />
<style>
.modal {
display: none; /* Hidden by default */
position: fixed; /* Stay in place */
z-index: 1; /* Sit on top */
padding-top: 100px; /* Location of the box */
left: 0;
top: 0;
width: 100%; /* Full width */
height: 100%; /* Full height */
overflow: auto; /* Enable scroll if needed */
background-color: rgb(0,0,0); /* Fallback color */
background-color: rgba(0,0,0,0.4); /* Black w/ opacity */
}
/* Modal Content */
.modal-content {
background-color: #fefefe;
margin: auto;
padding: 20px;
border: 1px solid #888;
width: 80%;
}
/* The Close Button */
.close {
color: #aaaaaa;
float: right;
font-size: 28px;
font-weight: bold;
}
.close:hover,
.close:focus {
color: #000;
text-decoration: none;
cursor: pointer;
}
.custom-btn {
transition: transform 0.3s ease, background-color 0.3s ease, border-color 0.3s ease;
color: #007BFF;
background-color: transparent;
padding: 5px 10px;
text-decoration: none;
display: inline-block;
font-size: 14px;
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;
}
</style>
</head>
<body class="light-mode animate">
<div class="container mt-4 table-container animate">
<h2 class="text-center">Statistiques du serveur</h2><br>
<div class="table-responsive">
<table class="table w-100">
<thead>
<tr>
<th>Paramètre</th>
<th>Valeur</th>
</tr>
</thead>
<tbody>
<tr>
<td>Temps de fonctionnement</td>
<td><%= Math.floor(uptime / 86400) %> jours, <%= Math.floor(uptime % 86400 / 3600) %> heures, <%= Math.floor(uptime % 3600 / 60) %> minutes, <%= uptime % 60 %> secondes</td>
</tr>
<tr>
<td>Utilisation de la mémoire</td>
<td><%= memoryUsage.toFixed(2) %> Mo</td>
</tr>
<tr>
<td>Utilisation du processeur</td>
<td><%= (cpuUsage * 100).toFixed(2) %> %</td>
</tr>
</tbody>
</table>
</div>
<h2>Journaux</h2>
<div class="row">
<% logs && logs.forEach((log, index) => { %>
<% if (log) { %>
<div class="col-md-4 text-center">
<button class="btn btn-primary custom-btn logName" data-index="<%= index %>"><%= log.name %></button>
</div>
<% if ((index + 1) % 3 === 0) { %>
</div><div class="row">
<% } %>
<div id="myModal<%= index %>" class="modal">
<div class="modal-content">
<span class="close" data-index="<%= index %>">&times;</span>
<pre><%= log.content %></pre>
</div>
</div>
<% } %>
<% }); %>
</div>
<div class="d-flex justify-content-center animate">
<button id="themeSwitcher" class="btn btn-warning mt-3 animate ">Changer de Thème</button>
</div>
<br><div class="d-flex justify-content-center align-items-center">
<a class="dropdown-item text-center text-white no-hover custom-btn" href="/dpanel/dashboard/admin/" style="max-width: 250px; padding: 10px;">
<i class="fas fa-sign-out-alt text-white "></i> Retourner au dashboard admin
</a>
</div>
<script>
var logNames = document.getElementsByClassName("logName");
var closeButtons = document.getElementsByClassName("close");
// Add event listeners to all log names
for (var i = 0; i < logNames.length; i++) {
logNames[i].addEventListener("click", function(event) {
var index = event.target.getAttribute("data-index");
var modal = document.getElementById("myModal" + index);
modal.style.display = "block";
});
}
// Add event listeners to all close buttons
for (var i = 0; i < closeButtons.length; i++) {
closeButtons[i].addEventListener("click", function(event) {
var index = event.target.getAttribute("data-index");
var modal = document.getElementById("myModal" + index);
modal.style.display = "none";
});
}
// When the user clicks anywhere outside of the modal, close it
window.onclick = function(event) {
if (event.target.className === "modal") {
event.target.style.display = "none";
}
}
const body = document.body;
document.getElementById('themeSwitcher').addEventListener('click', function () {
if (body.classList.contains('dark-mode')) {
body.classList.remove('dark-mode');
body.classList.add('light-mode');
} else {
body.classList.remove('light-mode');
body.classList.add('dark-mode');
}
});
document.addEventListener('DOMContentLoaded', function () {
const darkModeSwitch = document.getElementById('darkModeSwitch');
const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
body.classList.toggle('dark-mode', darkModeMediaQuery.matches);
darkModeMediaQuery.addListener(function (e) {
body.classList.toggle('dark-mode', e.matches);
});
});
</script>
</body>
</script>
</div>
</body>
</html>

193
views/paramAdminUser.ejs Normal file
View File

@@ -0,0 +1,193 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@10"></script>
<link rel="stylesheet" href="/public/css/login.css">
<title>Parameter Admin</title>
<link rel="icon" href="/public/assets/homelab_logo.png" />
</head>
<style>
.pagination .active .page-link {
background-color: #007bff !important;
color: white !important;
}
.table-container {
max-width: 90%;
}
#searchInput {
flex-grow: 1;
}
#searchButton {
margin-left: 10px;
}
.custom-btn {
transition: transform 0.3s ease, background-color 0.3s ease, border-color 0.3s ease;
color: #007BFF;
background-color: transparent;
padding: 5px 10px;
text-decoration: none;
display: inline-block;
font-size: 14px;
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;
}
</style>
<body class="light-mode animate">
<div class="container mt-4 table-container animate">
<h2 class="text-center">Gestion des utilisateurs</h2><br>
<div class="d-flex justify-content-between align-items-center">
<input type="text" id="searchInput" class="form-control rounded mr-2" placeholder="Rechercher par nom ou ID">
<span id="shortcutHint" class="mr-2 align-self-center"></span>
<button id="searchButton" class="btn btn-primary custom-btn">Rechercher</button>
<select id="usersPerPage" class="form-control rounded ml-2" style="width: auto;">
<option value="10">10</option>
<option value="50">50</option>
<option value="100">100</option>
<option value="500">500</option>
</select>
</div>
<div class="table-responsive mt-2">
<table class="table w-100 table table-hover">
<thead>
<tr>
<th>ID</th>
<th>Nom</th>
<th>Rôle</th>
<th class="text-right">Action</th>
</tr>
</thead>
<tbody>
<% users.forEach(user => { %>
<tr>
<td><%= user.id %></td>
<td><%= user.name %></td>
<td><%= user.role %></td>
<td>
<form action="/dpanel/dashboard/update-role" method="POST" class="d-flex align-items-center">
<input type="hidden" name="id" value="<%= user.id %>">
<input type="hidden" name="name" value="<%= user.name %>">
<select class="form-control rounded mr-2" name="role">
<option value="user" <%= user.role === 'user' ? 'selected' : '' %>>User</option>
<option value="admin" <%= user.role === 'admin' ? 'selected' : '' %>>Admin</option>
</select>
<button type="submit" class="btn btn-primary btn-round custom-btn">Mettre à jour</button>
</form>
</td>
</tr>
<% }) %>
</tbody>
</table>
</div>
<nav aria-label="Page navigation example">
<ul class="pagination justify-content-center">
<% for(let i = 1; i <= pages; i++) { %>
<li class="page-item <%= i === currentPage ? 'active' : '' %>">
<a class="page-link" href="/dpanel/dashboard/admin/users?page=<%= i %>&limit=<%= limit %>"><%= i %></a>
</li>
<% } %>
</ul>
</nav>
<div class="d-flex justify-content-center align-items-center">
<a class="dropdown-item text-center text-white no-hover custom-btn" href="/dpanel/dashboard/admin/" style="max-width: 250px; padding: 10px;">
<i class="fas fa-sign-out-alt text-white "></i> Retourner au dashboard admin
</a>
</div>
</div>
<div class="d-flex justify-content-center animate">
<button id="themeSwitcher" class="btn btn-warning mt-3 animate ">Changer de Thème</button>
</div>
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"></script>
<script>
const body = document.body;
document.getElementById('themeSwitcher').addEventListener('click', function () {
body.classList.toggle('dark-mode');
});
document.getElementById('usersPerPage').addEventListener('change', function () {
window.location.href = "/dpanel/dashboard/admin/users?page=1&limit=" + this.value;
});
window.onload = function() {
var urlParams = new URLSearchParams(window.location.search);
var limit = urlParams.get('limit');
document.getElementById('usersPerPage').value = limit;
}
document.addEventListener('DOMContentLoaded', function () {
const darkModeSwitch = document.getElementById('darkModeSwitch');
const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
body.classList.toggle('dark-mode', darkModeMediaQuery.matches);
darkModeMediaQuery.addListener(function (e) {
body.classList.toggle('dark-mode', e.matches);
});
});
function searchUsers() {
var input = document.getElementById('searchInput');
var filter = input.value.toUpperCase();
var table = document.querySelector('.table');
var tr = table.getElementsByTagName('tr');
for (var i = 0; i < tr.length; i++) {
var tdName = tr[i].getElementsByTagName('td')[1];
var tdId = tr[i].getElementsByTagName('td')[0];
if (tdName || tdId) {
var txtValueName = tdName.textContent || tdName.innerText;
var txtValueId = tdId.textContent || tdId.innerText;
if (txtValueName.toUpperCase().indexOf(filter) > -1 || txtValueId.toUpperCase().indexOf(filter) > -1) {
tr[i].style.display = "";
} else {
tr[i].style.display = "none";
}
}
}
}
document.getElementById('searchButton').addEventListener('click', function (e) {
e.preventDefault();
searchUsers();
});
document.addEventListener('keydown', function (e) {
if (e.ctrlKey && e.key === 'k') {
e.preventDefault();
document.getElementById('searchInput').focus();
}
});
var isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
var searchInput = document.getElementById('searchInput');
searchInput.placeholder = isMac ? 'Rechercher par nom ou ID (Cmd + K)' : 'Rechercher par nom ou ID (Ctrl + K)';
</script>
</body>
</html>

31
views/parametres.ejs Normal file
View File

@@ -0,0 +1,31 @@
<!DOCTYPE html>
<html>
<head>
<title>En maintenance...</title>
<link rel="icon" href="/public/assets/homelab_logo.png"/>
<style>
body {
background-color: #000;
text-align: center;
padding: 150px; }
h1 {
font-size: 50px; }
body {
font: 20px Helvetica, sans-serif; color: #fff;
}
img{
width : 300px;
height : 300px;
}
a{
color: #FFF
}
</style>
</head>
<body>
<img id="img" src="/client/assets/img/homelab_logo.png">
<h1>Désolé, nous sommes en maintenance.</h1>
<h2>Nous serons bientôt de retour..</h2>
</body>
</html>

123
views/password-check.ejs Normal file
View File

@@ -0,0 +1,123 @@
<!DOCTYPE html>
<html>
<head>
<title>Fichier sécurisé</title>
<link rel="icon" href="/public/assets/homelab_logo.png" />
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@10"></script>
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
</head>
<style>
.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">
<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>
</div>
<script>
$('#password-form').on('submit', function(e) {
e.preventDefault();
const userId = '<%= userId %>';
const filename = '<%= filename %>';
const enteredPassword = $('#password').val();
if (!enteredPassword) {
Swal.fire({
position: 'top',
icon: 'error',
title: 'Password is required',
showConfirmButton: false,
timer: 1800,
toast: true
});
return;
}
$.post('/attachments/' + userId + '/' + filename, { 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;
});
} else {
Swal.fire({
position: 'top',
icon: 'error',
title: 'Mot de passe incorrect',
showConfirmButton: false,
timer: 1800,
toast: true
});
}
})
.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
});
});
});
</script>
</body>
</html>

53
views/unauthorized.ejs Normal file
View File

@@ -0,0 +1,53 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Accès non autorisé</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f0f0f0;
text-align: center;
}
.container {
background-color: #fff;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
padding: 20px;
margin: 50px auto;
max-width: 400px;
}
h1 {
color: #e74c3c;
}
p {
color: #333;
}
.button {
display: inline-block;
padding: 10px 20px;
margin-top: 20px;
background-color: #3498db;
color: #fff;
text-decoration: none;
border-radius: 5px;
}
.button:hover {
background-color: #2980b9;
}
</style>
</head>
<body>
<div class="container">
<h1>Accès non autorisé</h1>
<p>Vous n'êtes pas autorisé à accéder à cette ressource.</p>
<a class="button" href="/">Retour à la page d'accueil</a>
</div>
</body>
</html>

147
views/upload.ejs Normal file
View File

@@ -0,0 +1,147 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
<link rel="stylesheet" href="/public/css/upload.css">
<link rel="stylesheet" href="/public/css/styles.css">
<title>Upload</title>
<link rel="icon" href="/public/assets/homelab_logo.png" />
</head>
<body class="light-mode">
<div class="container mt-4 flex-column">
<h1 class="mb-4">Upload de Fichiers</h1>
<form id="uploadForm" enctype="multipart/form-data">
<div class="form-group">
<label for="file">Sélectionnez un fichier :</label>
<input class="form-control" type="file" name="file" id="fileInput"
accept=".zip, .pdf, .txt, .jpg, .jpeg, .png, .gif, .iso, .mp4">
</div>
<div class="form-group">
<label for="expiryDate">Date d'expiration :</label>
<input class="form-control" type="date" name="expiryDate" id="expiryDate">
</div>
<div class="form-group">
<label for="password">Mot de passe :</label>
<input class="form-control" type="password" name="password" id="password">
</div>
</form>
<div class="progress mt-4 bg-white shadow-sm form-control">
<div class="progress-bar" id="progressBar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
</div>
<div class="mt-2 progress-info">
<small class="text-muted" id="progressText">0%</small>
<small class="text-muted ml-3" id="estimatedTime">Temps estimé : Calcul en cours...</small>
</div>
<div class="d-flex justify-content-between mt-4">
<button onclick="window.location.href='/dpanel/dashboard';" class="btn btn-primary mt-3 custom-btn">Retourner au Dashboard</button>
<div class="mx-auto">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="darkModeSwitch">
<button id="themeSwitcher" class="btn btn-warning mt-3 ml-2">Changer de Thème</button>
</div>
</div>
<button type="submit" form="uploadForm" class="btn btn-primary mt-3 custom-btn">Envoyer</button>
</div>
</div>
<script>
const progressBar = document.getElementById('progressBar');
const progressText = document.getElementById('progressText');
const estimatedTime = document.getElementById('estimatedTime');
const uploadForm = document.getElementById('uploadForm');
const body = document.body;
document.getElementById('themeSwitcher').addEventListener('click', function () {
body.classList.toggle('dark-mode');
});
uploadForm.addEventListener('submit', (event) => {
event.preventDefault();
const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
const expiryDate = document.getElementById('expiryDate').value;
const password = document.getElementById('password').value;
const formData = new FormData();
formData.append('file', file);
formData.append('expiryDate', expiryDate);
formData.append('password', password);
const xhr = new XMLHttpRequest();
xhr.open('POST', '/dpanel/upload', true);
xhr.upload.onprogress = (event) => {
if (event.lengthComputable) {
const percentage = Math.round((event.loaded / event.total) * 100);
const remainingTime = calculateRemainingTime(event.loaded, event.total, event.timeStamp);
updateProgress(percentage, remainingTime);
}
};
xhr.onload = () => {
if (xhr.status === 200) {
const response = JSON.parse(xhr.responseText);
const fileLink = response.fileLink;
window.location.href = '/dpanel/upload-success';
} else {
console.error('Erreur lors du téléchargement du fichier.', xhr.status);
}
};
xhr.send(formData);
});
function calculateRemainingTime(loaded, total, timeStamp) {
const bytesPerSecond = loaded / (timeStamp / 1000);
const remainingBytes = total - loaded;
const remainingSeconds = Math.round(remainingBytes / bytesPerSecond);
if (remainingSeconds < 60) {
return remainingSeconds + ' seconde' + (remainingSeconds !== 1 ? 's' : '');
} else if (remainingSeconds < 3600) {
const remainingMinutes = Math.floor(remainingSeconds / 60);
const remainingSecondsPart = remainingSeconds % 60;
return remainingMinutes + ' minute' + (remainingMinutes !== 1 ? 's' : '') + ' et ' + remainingSecondsPart + ' seconde' + (remainingSecondsPart !== 1 ? 's' : '');
} else {
const remainingHours = Math.floor(remainingSeconds / 3600);
const remainingMinutes = Math.floor((remainingSeconds % 3600) / 60);
const remainingSecondsPart = remainingSeconds % 60;
return remainingHours + ' heure' + (remainingHours !== 1 ? 's' : '') + ' ' + remainingMinutes + ' minute' + (remainingMinutes !== 1 ? 's' : '') + ' et ' + remainingSecondsPart + ' seconde' + (remainingSecondsPart !== 1 ? 's' : '');
}
}
function updateProgress(percentage, timeRemaining) {
progressBar.style.width = percentage + '%';
progressBar.setAttribute('aria-valuenow', percentage);
progressText.textContent = percentage + '%';
estimatedTime.textContent = 'Temps estimé : ' + timeRemaining;
}
document.addEventListener('DOMContentLoaded', function () {
const darkModeSwitch = document.getElementById('darkModeSwitch');
const body = document.body;
const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
body.classList.toggle('dark-mode', darkModeMediaQuery.matches);
darkModeMediaQuery.addListener(function (e) {
body.classList.toggle('dark-mode', e.matches);
});
});
</script>
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"></script>
</body>
</html>