Updated backend

- Added better anti DoS protection
- Added better security measures (HTTP headers, etc.)
- Added TLS support
- Added support for configurable rate limiting
- Added default 404 and error handling
- Updated proxy settings
- Updated env naming
This commit is contained in:
2024-03-31 20:50:58 +02:00
parent abd6f6747f
commit d93bfe333d
10 changed files with 147 additions and 34 deletions

View File

@@ -1,11 +1,22 @@
PORT=3000
PORT=80
DOMAIN="http://localhost:3000"
DATABASE_HOST="127.0.0.1"
DATABASE_NAME=hsp_gdh
DATABASE_USER=hsp_gdh
DATABASE_PASSWORD=""
JWT_SECRET=""
SMTP=
MAIL=
MAIL_PASS=
MAIL_SERVER=
MAIL_ADDRESS=
MAIL_PASSWORD=
BEHIND_PROXY=false
DISABLE_EMAIL_VERIFICATION=true
DISABLE_2_FACTOR_AUTHENTICATION=true
DISABLE_ANTI_DOS=true
DISABLE_ANTI_SPAM=true
RATE_LIMIT_REQUESTS=100
RATE_LIMIT_LOGIN_ATTEMPTS=5
RATE_LIMIT_VERIFICATION_REQUESTS=5
ENABLE_HTTPS=false
HTTPS_PORT=443
HTTPS_KEY=domain.key
HTTPS_CERT=domain.crt

5
.gitignore vendored
View File

@@ -136,4 +136,7 @@ logs/
*.log
# token
tokens/
tokens/
# certs
certs/

View File

@@ -0,0 +1,38 @@
# HSP-GDH (API)
Short description of your project.
## Table of Contents
- [HSP-GDH (API)](#hsp-gdh-api)
- [Table of Contents](#table-of-contents)
- [Installation](#installation)
- [Usage](#usage)
## Installation
1. Install [bun](https://bun.sh/).
2. Clone the repository.
3. Install the dependencies by running the following command:
```bash
bun install
```
4. Copy .env.sample as .env
```bash
cp .env.sample .env
```
5. (Optional) Setup SSL/TLS certs
1. Create Certificate
```bash
openssl req -newkey rsa:2048 -nodes -keyout certs/domain.key -x509 -days 365 -out certs/domain.crt -subj "/C=FR/L=Paris/O=HSP-GDH/CN=localhost"
```
2. Enable HTTPS in .env
## Usage
1. Start the application by running the following command:
```bash
bun index.js
```
2. Open your browser and navigate to `http(s)://localhost:{port}`.

BIN
bun.lockb

Binary file not shown.

View File

@@ -67,7 +67,7 @@ CREATE TABLE user_roles (
CREATE TABLE verification_codes (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
user_id INT UNSIGNED NOT NULL,
verification_code VARCHAR(255),
verification_code VARCHAR(255) NOT NULL,
type VARCHAR(32) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),

View File

@@ -1,12 +1,15 @@
import fs from 'fs';
import path from 'path';
import cors from 'cors';
import https from 'https';
import helmet from 'helmet';
import logger from 'morgan';
import express from 'express';
import cookieParser from 'cookie-parser';
import { log } from './modules/logManager';
import { speedLimiter, checkSystemLoad } from './modules/requestHandler';
import { fileExist } from './modules/fileManager';
import { log, error } from './modules/logManager';
import { speedLimiter, requestLimiter, checkSystemLoad, respondWithStatus } from './modules/requestHandler';
import testRouter from './routes/test';
import usersRouter from './routes/users';
@@ -21,17 +24,23 @@ const logsDir = path.join(import.meta.dir, 'logs');
if (!fs.existsSync(logsDir)) fs.mkdirSync(logsDir);
const app = express();
app.set('trust proxy', 1);
if (process.env.BEHIND_PROXY == 'true') app.set('trust proxy', ['loopback', 'linklocal', 'uniquelocal']);
app.use(cors({ origin: '*' }));
app.use(express.json());
app.use(cookieParser());
app.use(speedLimiter);
// Security
app.use(helmet());
app.disable('x-powered-by');
// Rate limiting and anti DoS
if (!process.env.DISABLE_ANTI_SPAM == 'true') app.use(speedLimiter);
if (!process.env.DISABLE_ANTI_DOS == 'true') app.use(requestLimiter);
app.use(checkSystemLoad);
// Logging
app.use(logger('dev'));
app.use(logger('combined', { stream: fs.createWriteStream(path.join(import.meta.dir, 'logs/access.log'), { flags: 'a' }) }));
app.use(cors({
origin: '*',
}));
app.use(express.static('public'));
@@ -44,11 +53,43 @@ app.use('/api/patients', patientsRouter);
app.use('/api/companies', companiesRouter);
app.use('/api/hospitals', hospitalsRouter);
// run the API
app.listen(process.env.PORT, async () => {
log(`running at port ${process.env.PORT}`);
app.use((req, res) => {
return respondWithStatus(res, 404, 'Nothing\'s here!');
});
// test
// import { post } from './modules/fetcher';
// post('http://127.0.0.1:1109/users/login', { 'usernameOrEmail':'foo', 'password':'bar' }).then(res => console.log(res));
app.use((err, req, res) => {
error(err.stack);
return respondWithStatus(res, 500, 'Internal server error');
});
// run the API server
if (process.env.ENABLE_HTTPS == 'true') {
const certsDir = path.join(import.meta.dir, 'certs');
if (!fs.existsSync(certsDir)) fs.mkdirSync(certsDir);
if (!fileExist(path.join(certsDir, process.env.HTTPS_KEY)) || !fileExist(path.join(certsDir, process.env.HTTPS_CERT))) {
error('Missing HTTPS key or certificate');
process.exit(1);
}
const options = {
key: fs.readFileSync(path.join(certsDir, process.env.HTTPS_KEY)),
cert: fs.readFileSync(path.join(certsDir, process.env.HTTPS_CERT)),
maxVersion: 'TLSv1.3',
minVersion: 'TLSv1.2',
ciphers: 'TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256',
ecdhCurve: 'P-521:P-384',
sigalgs: 'ecdsa_secp384r1_sha384',
honorCipherOrder: true,
};
https.createServer(options, app).listen(process.env.HTTPS_PORT, async () => {
log('running in HTTPS mode');
log(`running at port ${process.env.HTTPS_PORT}`);
});
}
else {
app.listen(process.env.PORT, async () => {
log('running in HTTP mode');
log(`running at port ${process.env.PORT}`);
});
}

View File

@@ -3,12 +3,12 @@ import { random } from './random';
import { log } from './logManager';
const transporter = nodemailer.createTransport({
host: process.env.SMTP,
host: process.env.MAIL_SERVER,
port: 465,
secure: true,
auth: {
user: `${process.env.MAIL}`,
pass: `${process.env.PASS}`,
user: `${process.env.MAIL_ADDRESS}`,
pass: `${process.env.PASSWORD}`,
},
tls: { rejectUnauthorized: false },
});

View File

@@ -6,7 +6,7 @@ import { log } from './logManager';
const requestLimiter = rateLimit({
windowMs: 60 * 1000,
max: 5,
max: process.env.RATE_LIMIT_REQUESTS || 100,
standardHeaders: true,
legacyHeaders: false,
message: 'Too many requests from this IP, please try again later',
@@ -18,6 +18,22 @@ const speedLimiter = slowDown({
delayMs: (hits) => hits * 100,
});
const antiBruteForce = rateLimit({
windowMs: 60 * 60 * 1000,
max: process.env.RATE_LIMIT_LOGIN_ATTEMPTS || 5,
standardHeaders: true,
legacyHeaders: false,
message: 'Too many login attempts, please try again later',
});
const antiVerificationSpam = rateLimit({
windowMs: 60 * 1000,
max: process.env.RATE_LIMIT_VERIFICATION_REQUESTS || 5,
standardHeaders: true,
legacyHeaders: false,
message: 'Too many verification requests, please try again later',
});
function checkSystemLoad(req, res, next) {
const load = os.loadavg()[0];
const cores = os.cpus().length;
@@ -49,6 +65,8 @@ function respondWithStatusJSON(res, statusCode, JSON) {
export {
requestLimiter,
antiBruteForce,
antiVerificationSpam,
speedLimiter,
checkSystemLoad,
respondWithStatus,

View File

@@ -17,9 +17,11 @@
"dependencies": {
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"express": "^4.18.2",
"express": "^4.19.2",
"express-rate-limit": "^7.1.4",
"express-slow-down": "^2.0.1",
"helmet": "^7.1.0",
"https": "^1.0.0",
"jsonwebtoken": "^9.0.2",
"level": "^8.0.1",
"morgan": "^1.10.0",

View File

@@ -4,12 +4,12 @@ import { pool } from '../modules/databaseManager';
import { sendVerification } from '../modules/mailHandler';
import { verifyToken, generateToken } from '../modules/tokenManager';
import { isEmailDomainValid, isValidEmail, isPhoneNumber } from '../modules/formatManager';
import { requestLimiter, respondWithStatus, respondWithStatusJSON } from '../modules/requestHandler';
import { antiBruteForce, antiVerificationSpam, respondWithStatus, respondWithStatusJSON } from '../modules/requestHandler';
import { checkBanned, checkPermissions, userExists, isBanned, verifyPermissions } from '../modules/permissionManager';
const router = express.Router();
router.post('/register', requestLimiter, async (req, res) => {
router.post('/register', antiBruteForce, async (req, res) => {
const { username, email, password, first_name, last_name, phone = null } = req.body;
if ([ username, email, password, first_name, last_name ].every(Boolean)) {
try {
@@ -30,7 +30,7 @@ router.post('/register', requestLimiter, async (req, res) => {
);
if (result.affectedRows === 0) return await respondWithStatus(res, 500, 'Error storing user');
if (process.env.DISABLE_EMAIL_VERIFICATION) return await respondWithStatus(res, 200, 'Successfully registered');
if (process.env.DISABLE_EMAIL_VERIFICATION == 'true') return await respondWithStatus(res, 200, 'Successfully registered');
const [rows] = await pool.execute('SELECT id FROM users WHERE email = ? LIMIT 1', [email]);
const code = sendVerification(email, rows[0].id, 'email');
@@ -51,7 +51,7 @@ router.post('/register', requestLimiter, async (req, res) => {
}
});
router.post('/login', requestLimiter, async (req, res) => {
router.post('/login', antiBruteForce, async (req, res) => {
const { usernameOrEmail, password } = req.body;
if ([usernameOrEmail, password].every(Boolean)) {
try {
@@ -76,7 +76,7 @@ router.post('/login', requestLimiter, async (req, res) => {
email: user.email,
first_name: user.first_name,
last_name: user.last_name,
verified_status: process.env.DISABLE_EMAIL_VERIFICATION ? true : user.email_verified,
verified_status: process.env.DISABLE_EMAIL_VERIFICATION == 'true' ? true : user.email_verified,
},
});
}
@@ -124,7 +124,7 @@ router.post('/', verifyToken, checkBanned, checkPermissions('user', 2), async (r
});
// Email verification endpoints
router.get('/email/request', verifyToken, checkBanned, async (req, res) => {
router.get('/email/request', antiVerificationSpam, verifyToken, checkBanned, async (req, res) => {
const userId = req.userId;
try {
const [rows] = await pool.execute('SELECT * FROM users WHERE id = ? LIMIT 1', [userId]);
@@ -141,7 +141,7 @@ router.get('/email/request', verifyToken, checkBanned, async (req, res) => {
}
});
router.get('/email/verify', verifyToken, checkBanned, async (req, res) => {
router.get('/email/verify', antiVerificationSpam, verifyToken, checkBanned, async (req, res) => {
const { code } = req.query;
const userId = req.userId;
if (code) {
@@ -173,7 +173,7 @@ router.get('/email/verify', verifyToken, checkBanned, async (req, res) => {
// PATCH /phone/verify
// Password reset endpoints
router.post('/password/request', async (req, res) => {
router.post('/password/request', antiVerificationSpam, async (req, res) => {
const { usernameOrEmail } = req.body;
if (usernameOrEmail) {
try {
@@ -204,7 +204,7 @@ router.post('/password/request', async (req, res) => {
}
});
router.patch('/password/verify', async (req, res) => {
router.patch('/password/verify', antiVerificationSpam, async (req, res) => {
const { usernameOrEmail, password, code } = req.body;
if ([usernameOrEmail, password, code].every(Boolean)) {
try {