Full commit for review

This commit is contained in:
2023-05-10 20:43:45 +02:00
parent bb6db9d523
commit 513772dd1e
47 changed files with 7956 additions and 1 deletions

27
api/modules/database.js Normal file
View File

@@ -0,0 +1,27 @@
const mysql = require('mysql2');
const mysql_promise = require('mysql2/promise');
const connection = mysql.createConnection({
host: process.env.DATABASE_HOST,
user: process.env.DATABASE_USER,
password: process.env.DATABASE_PASSWORD,
database: process.env.DATABASE_NAME,
});
const pool = mysql_promise.createPool({
host: process.env.DATABASE_HOST,
user: process.env.DATABASE_USER,
password: process.env.DATABASE_PASSWORD,
database: process.env.DATABASE_NAME,
});
function createPool(host, user, password, db) {
const newPool = mysql_promise.createPool({
host: host,
user: user,
password: password,
database: db,
});
return newPool;
}
module.exports = { connection, pool, createPool };

71
api/modules/fetcher.js Normal file
View File

@@ -0,0 +1,71 @@
const fetch = require('node-fetch');
const { error } = require('./log');
async function get(url, token) {
const options = {
method: 'GET',
headers: { 'Content-Type': 'application/json', authorization: `${token}` },
};
return await fetch(url, options)
.then(res => res.json())
.then(json => {
return json;
})
.catch(err => error(err));
}
async function post(url, body, token) {
const options = {
method: 'POST',
mode: 'cors',
headers: { 'Content-Type': 'application/json', authorization: `${token}` },
body: JSON.stringify(body),
};
return await fetch(url, options)
.then(res => res.json())
.then(json => {
return json;
})
.catch(err => error(err));
}
async function patch(url, body, token) {
const options = {
method: 'PATCH',
mode: 'cors',
headers: { 'Content-Type': 'application/json', authorization: `${token}` },
body: JSON.stringify(body),
};
return await fetch(url, options)
.then(res => res.json())
.then(json => {
return json;
})
.catch(err => error(err));
}
async function put(url, body, token) {
const options = {
method: 'PUT',
mode: 'cors',
headers: { 'Content-Type': 'application/json', authorization: `${token}` },
body: JSON.stringify(body),
};
return await fetch(url, options)
.then(res => res.json())
.then(json => {
return json;
})
.catch(err => error(err));
}
module.exports = {
get,
post,
patch,
put,
};

View File

@@ -0,0 +1,79 @@
const fs = require('fs');
const download = require('download');
const random = require('./random');
function fileExist(path) {
try {
fs.readFileSync(path);
return true;
}
catch (err) {
return false;
}
}
function fileDelete(path) {
try {
fs.unlinkSync(path);
return true;
}
catch (err) {
return false;
}
}
function fileDownload(url, name) {
try {
download(url, '../cdn/images/', { filename: name });
return true;
}
catch (err) {
return false;
}
}
function folderExist(path) {
try {
if (fs.existsSync(path)) {
return true;
}
else {
return false;
}
}
catch (err) {
return false;
}
}
function getFilesFromFolder(path) {
try {
return fs.readdirSync(path);
}
catch (err) {
return false;
}
}
function randomFileFromFolder(path) {
try {
if (getFilesFromFolder(path)) {
return random.random(0, getFilesFromFolder(path).length);
}
else {
return false;
}
}
catch (err) {
return false;
}
}
module.exports = {
fileExist,
fileDelete,
fileDownload,
folderExist,
getFilesFromFolder,
randomFileFromFolder,
};

View File

@@ -0,0 +1,31 @@
const dns = require('dns');
function isEmailDomainValid(email) {
const domain = email.split('@')[1];
return new Promise((resolve, reject) => {
dns.lookup(domain, (err, address) => {
if (err) {
reject(err);
}
else {
const isIPAddress = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(address);
resolve(isIPAddress);
}
});
});
}
function isValidEmail(email) {
const emailRegex = /\S+@\S+\.\S+/;
return emailRegex.test(email);
}
function isNumber(x) {
return /^-?[\d.]+(?:e-?\d+)?$/.test(x);
}
module.exports = {
isEmailDomainValid,
isValidEmail,
isNumber,
};

26
api/modules/log.js Normal file
View File

@@ -0,0 +1,26 @@
const pino = require('pino');
const logger = pino();
function log(x) {
logger.info(x);
}
function debug(x) {
logger.debug(x);
}
function warn(x) {
logger.warn(x);
}
function error(x) {
logger.error(x);
}
module.exports = {
log,
debug,
warn,
error,
};

100
api/modules/mailHandler.js Normal file
View File

@@ -0,0 +1,100 @@
const argon2 = require('argon2');
const nodemailer = require('nodemailer');
const { random } = require('./random');
const { createPool } = require('./database');
const pool = createPool('localhost', 'root', '', 'postfixadmin');
const transporter = nodemailer.createTransport({
host: process.env.SMTP,
port: 465,
secure: true,
auth: {
user: process.env.MAIL,
pass: process.env.MAIL_PASS,
},
});
async function createAddress(username, domain, password, name = '', backup_email = '', phone = '') {
try {
const hashedPassword = await argon2.hash(password, {
type: argon2.argon2i,
memoryCost: 2 ** 15,
hashLength: 32,
timeCost: 5,
});
const [result] = await pool.execute(
'INSERT INTO `mailbox` (`username`, `password`, `name`, `maildir`, `quota`, `local_part`, `domain`, `created`, `modified`, `active`, `phone`, `email_other`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
[ username, hashedPassword, name, `${domain}/${username}`, 0, username, domain, Date.now(), Date.now(), '1', phone, backup_email],
);
if (result.affectedRows === 0) {
return false;
}
return true;
}
catch (error) {
console.error(error);
return false;
}
}
function sendMail(email, head, body) {
try {
// setup email data
const mailOptions = {
from: `"AirJet" <${process.env.MAIL}>`,
to: email,
subject: head,
text: body,
};
// send mail with defined transport object
transporter.sendMail(mailOptions, (error, info) => {
if (error) {
console.log(error);
}
else {
console.log('Email sent: ' + info.response);
}
});
return true;
}
catch (err) {
return false;
}
}
function sendVerification(email, userId) {
try {
const code = random(100000, 999999);
if (sendMail(email, 'Your verification code for AirJet', `Verification code: ${code}\nLink: https://aostia.me/api/users/verify?u=${userId}&c=${code}`)) {
return code;
}
else {
return false;
}
}
catch (err) {
return false;
}
}
function sendResetVerification(email, code = random(100000, 999999)) {
try {
if (sendMail(email, 'Your reset verification code for AirJet', `Verification code: ${code}`)) {
return code;
}
else {
return false;
}
}
catch (err) {
return false;
}
}
module.exports = {
sendMail,
sendVerification,
sendResetVerification,
createAddress,
};

101
api/modules/permission.js Normal file
View File

@@ -0,0 +1,101 @@
const { pool } = require('../modules/database.js');
const { respondWithStatus } = require('./requestHandler');
// Middleware to verify the user permissions
async function verifyPermissions(userId, perms_req) {
try {
// Query the database to get the user
const [user] = await pool.execute('SELECT * FROM users WHERE id = ? LIMIT 1', [userId]);
if (user.length === 0) {
return false;
}
// Query the database to get the perms and verify
const [hasPerm] = await pool.execute(
'SELECT COUNT(*) AS count FROM user_type_permissions WHERE user_type_id = ? AND permission_id = (SELECT id FROM permissions WHERE name = ?) LIMIT 1',
[ user[0].user_type_id, perms_req ],
);
if (hasPerm.length === 0) {
return false;
}
else {
return true;
}
}
catch (error) {
return false;
}
}
const hasPermission = (perms_req) => async (req, res, next) => {
try {
const userId = req.userId;
// Query the database to get the user
const [user] = await pool.execute('SELECT * FROM users WHERE id = ? LIMIT 1', [userId]);
if (user.length === 0) {
return await respondWithStatus(res, 401, 'User is invalid');
}
// Query the database to get the perms and verify
const [hasPerm] = await pool.execute(
'SELECT COUNT(*) AS count FROM user_type_permissions WHERE user_type_id = ? AND permission_id = (SELECT id FROM permissions WHERE name = ?) LIMIT 1',
[ user[0].user_type_id, perms_req ],
);
if (req.originalUrl == '/api/users/me') {
next();
return;
}
if (hasPerm.length === 0) {
return await respondWithStatus(res, 403, 'Missing permission');
}
else if (hasPerm[0].count == 0) {
return await respondWithStatus(res, 403, 'Missing permission');
}
else {next();}
}
catch (error) {
console.error(error);
return await respondWithStatus(res, 500, 'An error has occured');
}
};
async function checkBanned(req, res, next) {
const userId = req.userId;
try {
const [user] = await pool.execute('SELECT * FROM users WHERE id = ? LIMIT 1', [userId]);
if (user.length === 0) {
return await respondWithStatus(res, 404, 'User not found');
}
if (user[0].is_banned) {
return await respondWithStatus(res, 403, 'User is banned');
}
next();
}
catch (error) {
console.error(error);
return await respondWithStatus(res, 500, 'An error has occured');
}
}
async function isBanned(userId) {
try {
// Query the database to get the user
const [user] = await pool.execute('SELECT * FROM users WHERE id = ? LIMIT 1', [userId]);
if (user.length === 0) {
return true;
}
else if (user[0].is_banned == 1) {
return true;
}
else {
return false;
}
}
catch (error) {
console.error(error);
return true;
}
}
module.exports = { verifyPermissions, hasPermission, checkBanned, isBanned };

14
api/modules/random.js Normal file
View File

@@ -0,0 +1,14 @@
const crypto = require('crypto');
function random(x, y) {
return crypto.randomInt(x, y);
}
function randomHEX(x) {
return crypto.randomBytes(x).toString('hex');
}
module.exports = {
random,
randomHEX,
};

View File

@@ -0,0 +1,55 @@
const rateLimit = require('express-rate-limit');
const slowDown = require('express-slow-down');
const http = require('http');
const os = require('os');
const requestLimiter = rateLimit({
windowMs: 60 * 1000,
max: 5,
standardHeaders: true,
legacyHeaders: false,
message: 'Too many requests from this IP, please try again later',
});
const speedLimiter = slowDown({
windowMs: 60 * 1000,
delayAfter: 30,
delayMs: 500,
skipFailedRequests: true,
});
function checkSystemLoad(req, res, next) {
const load = os.loadavg()[0];
const cores = os.cpus().length;
const threshold = cores * 0.7;
if (load > threshold) {
return res.status(503).send(http.STATUS_CODES[503]);
}
return next();
}
function respondWithStatus(res, statusCode, message) {
const response = { status: statusCode, message: message };
if (statusCode >= 400 && statusCode <= 599) {
response.error = http.STATUS_CODES[statusCode];
}
return res.status(statusCode).json(response);
}
function respondWithStatusJSON(res, statusCode, JSON) {
const response = { status: statusCode, JSON };
if (statusCode >= 400 && statusCode <= 599) {
response.error = http.STATUS_CODES[statusCode];
}
return res.status(statusCode).json(response);
}
module.exports = {
requestLimiter,
speedLimiter,
checkSystemLoad,
respondWithStatus,
respondWithStatusJSON,
};

103
api/modules/token.js Normal file
View File

@@ -0,0 +1,103 @@
const jwt = require('jsonwebtoken');
// const { Level } = require('level');
const levelup = require('levelup');
const leveldown = require('leveldown');
const argon2 = require('argon2');
const { respondWithStatus } = require('./requestHandler');
const { pool } = require('./database');
// Set up LevelDB instance
// const db = new Level('./tokensDB');
const db = levelup(leveldown('./tokensDB'));
// Generate a new JWT
const generateToken = async (userId, password) => {
const token = jwt.sign({ userId: userId, password: password }, process.env.JWT_SECRET, { expiresIn: '7d' });
await db.put(token, 'valid');
return token;
};
// Middleware to verify the JWT and set req.userId
const verifyToken = async (req, res, next) => {
const token = req.headers.authorization;
if (!token) {
return await respondWithStatus(res, 401, 'No token provided');
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.userId = decoded.userId;
const [rows] = await pool.execute(
'SELECT * FROM users WHERE id = ? LIMIT 1',
[req.userId],
);
if (!rows.length) {
return await respondWithStatus(res, 404, 'User not found!');
}
const passwordMatch = await argon2.verify(rows[0].password, decoded.password);
if (!passwordMatch) {
return await respondWithStatus(res, 401, 'Token is invalid');
}
// Check if the token is close to expiring
const now = Date.now().valueOf() / 1000;
if (decoded.exp - now < 36000) {
// Generate a new token if the old one is close to expiring
const newToken = generateToken(req.userId, decoded.password);
res.cookie('token', newToken, {
expires: new Date(Date.now() + 14 * 24 * 60 * 60 * 1000),
httpOnly: true,
secure: true,
sameSite: 'strict',
});
res.set('Authorization', newToken);
}
// Check if the token has been revoked
const tokenStatus = await db.get(token);
if (tokenStatus != 'valid') {
return await respondWithStatus(res, 401, 'Token has been revoked ');
}
next();
}
catch (error) {
return await respondWithStatus(res, 401, 'Invalid user');
}
};
// Function to revoke a token
const revokeToken = (token) => {
return new Promise((resolve, reject) => {
db.put(token, 'revoked', (error) => {
if (error) {
reject(error);
}
else {
resolve();
}
});
});
};
// Function to revoke all tokens of a user
const revokeUserTokens = (userId) => {
return new Promise((resolve, reject) => {
const tokensToRevoke = [];
db.createReadStream()
.on('data', (data) => {
const token = data.key;
const decoded = jwt.decode(token);
if (decoded.userId === userId) {
tokensToRevoke.push(token);
}
})
.on('end', () => {
Promise.all(tokensToRevoke.map(revokeToken))
.then(resolve)
.catch(reject);
});
});
};
module.exports = { generateToken, verifyToken, revokeToken, revokeUserTokens };