First part of backend rework
- Added the base data structure for the new database - Added the new routes for the new database - Reworked the users endpoints
This commit is contained in:
48
modules/cronManager.js
Normal file
48
modules/cronManager.js
Normal file
@@ -0,0 +1,48 @@
|
||||
import cron from 'node-cron';
|
||||
|
||||
const daysOfWeek = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'];
|
||||
const hoursOfDay = [...Array(24).keys()].map(hour => hour.toString());
|
||||
const minutesOfHour = [...Array(60).keys()].map(minute => minute.toString());
|
||||
|
||||
function toCronExpression(str) {
|
||||
switch (str.toLowerCase()) {
|
||||
case 'everyfiveseconds': {
|
||||
return '*/5 * * * * *';
|
||||
}
|
||||
case 'everythirtyseconds': {
|
||||
return '*/30 * * * * *';
|
||||
}
|
||||
case 'everyminute': {
|
||||
return '* * * * *';
|
||||
}
|
||||
case 'everyfiveminutes': {
|
||||
return '*/5 * * * *';
|
||||
}
|
||||
case 'everyfifteenminutes': {
|
||||
return '*/15 * * * *';
|
||||
}
|
||||
case 'everythirtyminutes': {
|
||||
return '*/30 * * * *';
|
||||
}
|
||||
case 'everyday': {
|
||||
return '0 0 * * *';
|
||||
}
|
||||
case 'weekdays': {
|
||||
return '0 0 * * 1-5';
|
||||
}
|
||||
case 'weekends': {
|
||||
return '0 0 * * 6,7';
|
||||
}
|
||||
default: {
|
||||
const [day, hour, minute] = str.toLowerCase().split(' ');
|
||||
const dayIndex = daysOfWeek.indexOf(day);
|
||||
const hourIndex = hoursOfDay.indexOf(hour);
|
||||
const minuteIndex = minutesOfHour.indexOf(minute);
|
||||
return `${minuteIndex} ${hourIndex} * * ${dayIndex}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function createCron(rate, exec) {
|
||||
cron.schedule(toCronExpression(rate), exec);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import fetch from 'node-fetch';
|
||||
import { error } from './log';
|
||||
import { error } from './logManager';
|
||||
|
||||
async function get(url, token) {
|
||||
async function get(url, token = 'none') {
|
||||
const options = {
|
||||
method: 'GET',
|
||||
headers: { 'Content-Type': 'application/json', authorization: `${token}` },
|
||||
@@ -15,7 +15,7 @@ async function get(url, token) {
|
||||
.catch(err => error(err));
|
||||
}
|
||||
|
||||
async function post(url, body, token) {
|
||||
async function post(url, body, token = 'none') {
|
||||
const options = {
|
||||
method: 'POST',
|
||||
mode: 'cors',
|
||||
@@ -31,7 +31,7 @@ async function post(url, body, token) {
|
||||
.catch(err => error(err));
|
||||
}
|
||||
|
||||
async function patch(url, body, token) {
|
||||
async function patch(url, body, token = 'none') {
|
||||
const options = {
|
||||
method: 'PATCH',
|
||||
mode: 'cors',
|
||||
@@ -47,7 +47,7 @@ async function patch(url, body, token) {
|
||||
.catch(err => error(err));
|
||||
}
|
||||
|
||||
async function put(url, body, token) {
|
||||
async function put(url, body, token = 'none') {
|
||||
const options = {
|
||||
method: 'PUT',
|
||||
mode: 'cors',
|
||||
@@ -1,6 +1,7 @@
|
||||
import fs from 'fs';
|
||||
import download from 'download';
|
||||
import random from './random';
|
||||
import { Readable } from 'stream';
|
||||
import { finished } from 'stream/promises';
|
||||
import { random } from './random';
|
||||
|
||||
function fileExist(path) {
|
||||
try {
|
||||
@@ -22,9 +23,11 @@ function fileDelete(path) {
|
||||
}
|
||||
}
|
||||
|
||||
function fileDownload(url, name) {
|
||||
async function fileDownload(url, path) {
|
||||
try {
|
||||
download(url, '../cdn/images/', { filename: name });
|
||||
const stream = fs.createWriteStream(path);
|
||||
const { body } = await fetch(url);
|
||||
await finished(Readable.fromWeb(body).pipe(stream));
|
||||
return true;
|
||||
}
|
||||
catch (err) {
|
||||
@@ -32,6 +35,7 @@ function fileDownload(url, name) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function folderExist(path) {
|
||||
try {
|
||||
if (fs.existsSync(path)) {
|
||||
@@ -58,7 +62,7 @@ function getFilesFromFolder(path) {
|
||||
function randomFileFromFolder(path) {
|
||||
try {
|
||||
if (getFilesFromFolder(path)) {
|
||||
return random.random(0, getFilesFromFolder(path).length);
|
||||
return random(0, getFilesFromFolder(path).length);
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
|
||||
@@ -22,4 +22,9 @@ export function isValidEmail(email) {
|
||||
|
||||
export function isNumber(x) {
|
||||
return /^-?[\d.]+(?:e-?\d+)?$/.test(x);
|
||||
}
|
||||
|
||||
export function isPhoneNumber(phone) {
|
||||
const phoneRegex = /^\d{10}$/;
|
||||
return phoneRegex.test(phone);
|
||||
}
|
||||
@@ -1,9 +1,6 @@
|
||||
/* eslint-disable no-undef */
|
||||
import nodemailer from 'nodemailer';
|
||||
import { random } from './random';
|
||||
import { createPool } from './database';
|
||||
|
||||
const pool = createPool('localhost', 'root', '', 'postfixadmin');
|
||||
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: process.env.SMTP,
|
||||
@@ -15,29 +12,6 @@ const transporter = nodemailer.createTransport({
|
||||
},
|
||||
});
|
||||
|
||||
async function createAddress(username, domain, password, name = '', backup_email = '', phone = '') {
|
||||
try {
|
||||
const hashedPassword = await Bun.password.hash(password, {
|
||||
type: Bun.password.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
|
||||
@@ -63,30 +37,29 @@ function sendMail(email, head, body) {
|
||||
}
|
||||
}
|
||||
|
||||
function sendVerification(email, userId) {
|
||||
function sendVerification(email, userId, type = 'register', code = null) {
|
||||
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 {
|
||||
code ? code : random(100000, 999999);
|
||||
let title, body;
|
||||
switch (type) {
|
||||
case 'email':
|
||||
title = 'Your verification code for HSP-GDH';
|
||||
body = `Verification code: ${code}\nLink: ${process.env.DOMAIN}/email/verify?code=${code}`;
|
||||
break;
|
||||
case 'password':
|
||||
title = 'Your password reset code for HSP-GDH';
|
||||
body = `Verification code: ${code}\nLink: ${process.env.DOMAIN}/password/reset?u=${userId}&c=${code}`;
|
||||
break;
|
||||
case '2fa':
|
||||
title = 'Your 2FA code for HSP-GDH';
|
||||
body = `Verification code: ${code}`;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
if (sendMail(email, title, body)) return code;
|
||||
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;
|
||||
}
|
||||
@@ -95,6 +68,4 @@ function sendResetVerification(email, code = random(100000, 999999)) {
|
||||
export {
|
||||
sendMail,
|
||||
sendVerification,
|
||||
sendResetVerification,
|
||||
createAddress,
|
||||
};
|
||||
@@ -1,101 +0,0 @@
|
||||
import { pool } from '../modules/database.js';
|
||||
import { respondWithStatus } from './requestHandler.js';
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
export { verifyPermissions, hasPermission, checkBanned, isBanned };
|
||||
65
modules/permissionManager.js
Normal file
65
modules/permissionManager.js
Normal file
@@ -0,0 +1,65 @@
|
||||
import { pool } from './databaseManager.js';
|
||||
import { respondWithStatus } from './requestHandler.js';
|
||||
import { error } from './logManager.js';
|
||||
|
||||
|
||||
export async function userExists(userId) {
|
||||
try {
|
||||
const [user] = await pool.execute('SELECT * FROM users WHERE id = ? LIMIT 1', [userId]);
|
||||
if (user.length === 0) return false;
|
||||
return true;
|
||||
}
|
||||
catch (err) {
|
||||
error(err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function isBanned(userId) {
|
||||
try {
|
||||
const [bannedUser] = await pool.execute('SELECT * FROM bans WHERE user_id = ? LIMIT 1', [userId]);
|
||||
if (bannedUser.length > 0) return true;
|
||||
return false;
|
||||
}
|
||||
catch (err) {
|
||||
error(err);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// permissionType is bitfield
|
||||
// 1 = read
|
||||
// 2 = write
|
||||
// 4 = delete
|
||||
export async function verifyPermissions(userId, permissionName, permissionType) {
|
||||
try {
|
||||
const [perms] = await pool.execute(`SELECT r.${permissionName}_bitfield AS permissionBitfield FROM users u INNER JOIN user_roles ur ON u.id = ur.user_id INNER JOIN roles r ON ur.role_id = r.id WHERE u.id = ?`, [userId]);
|
||||
for (const row of perms) {
|
||||
if (row.permissionBitfield & permissionType) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
error(err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkUserExists(req, res, next) {
|
||||
const userId = req.userId;
|
||||
if (!userExists(userId)) return await respondWithStatus(res, 404, 'User not found');
|
||||
next();
|
||||
}
|
||||
|
||||
export async function checkBanned(req, res, next) {
|
||||
const userId = req.userId;
|
||||
if (isBanned(userId)) return await respondWithStatus(res, 403, 'User is banned');
|
||||
next();
|
||||
}
|
||||
|
||||
export const checkPermissions = (permissionName, permissionType) => async (req, res, next) => {
|
||||
const userId = req.userId;
|
||||
if (!verifyPermissions(userId, permissionName, permissionType)) return await respondWithStatus(res, 403, 'Missing permission');
|
||||
next();
|
||||
};
|
||||
@@ -1,9 +1,22 @@
|
||||
import crypto from 'crypto';
|
||||
|
||||
export function random(x, y) {
|
||||
return crypto.randomInt(x, y);
|
||||
return crypto.randomInt(x, y + 1);
|
||||
}
|
||||
|
||||
export function random2(min, max) {
|
||||
const range = max - min + 1;
|
||||
const byteLength = Math.ceil(Math.log2(range) / 8);
|
||||
let randomValue;
|
||||
|
||||
do {
|
||||
const randomBytes = crypto.randomBytes(byteLength);
|
||||
randomValue = parseInt(randomBytes.toString('hex'), 16);
|
||||
} while (randomValue >= range);
|
||||
|
||||
return randomValue + min;
|
||||
}
|
||||
|
||||
export function randomHEX(x) {
|
||||
return crypto.randomBytes(x).toString('hex');
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import rateLimit from 'express-rate-limit';
|
||||
import slowDown from 'express-slow-down';
|
||||
import http from 'http';
|
||||
import os from 'os';
|
||||
import { log } from './logManager';
|
||||
|
||||
const requestLimiter = rateLimit({
|
||||
windowMs: 60 * 1000,
|
||||
@@ -23,7 +24,7 @@ function checkSystemLoad(req, res, next) {
|
||||
const threshold = cores * 0.7;
|
||||
|
||||
if (load > threshold) {
|
||||
console.log('System load too high, please try again later');
|
||||
log('System load too high, please try again later');
|
||||
return res.status(503).send(http.STATUS_CODES[503]);
|
||||
}
|
||||
|
||||
|
||||
101
modules/token.js
101
modules/token.js
@@ -1,101 +0,0 @@
|
||||
/* eslint-disable no-undef */
|
||||
import jwt from 'jsonwebtoken';
|
||||
import levelup from 'levelup';
|
||||
import leveldown from 'leveldown';
|
||||
import { respondWithStatus } from './requestHandler';
|
||||
import { pool } from './database';
|
||||
|
||||
|
||||
// Set up LevelDB instance
|
||||
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 Bun.password.verify(decoded.password, rows[0].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);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export { generateToken, verifyToken, revokeToken, revokeUserTokens };
|
||||
40
modules/tokenManager.js
Normal file
40
modules/tokenManager.js
Normal file
@@ -0,0 +1,40 @@
|
||||
/* eslint-disable no-undef */
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { Level } from 'level';
|
||||
import { respondWithStatus } from './requestHandler';
|
||||
import { userExists } from './permissionManager';
|
||||
|
||||
const db = new Level('tokens', { valueEncoding: 'json' });
|
||||
|
||||
export async function generateToken(userId, password) {
|
||||
const token = jwt.sign({ userId: userId, password: password }, process.env.JWT_SECRET, { expiresIn: '7d' });
|
||||
await db.put(token, 'valid');
|
||||
return token;
|
||||
}
|
||||
|
||||
|
||||
export async function verifyToken(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;
|
||||
|
||||
if (!userExists(userId)) return await respondWithStatus(res, 404, 'User not found');
|
||||
const passwordMatch = await Bun.password.verify(decoded.password, rows[0].password);
|
||||
if (!passwordMatch) return await respondWithStatus(res, 401, 'Token is invalid');
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
export async function revokeToken(token) {
|
||||
db.put(token, 'revoked');
|
||||
}
|
||||
Reference in New Issue
Block a user