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:
19
.env.sample
19
.env.sample
@@ -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
5
.gitignore
vendored
@@ -136,4 +136,7 @@ logs/
|
||||
*.log
|
||||
|
||||
# token
|
||||
tokens/
|
||||
tokens/
|
||||
|
||||
# certs
|
||||
certs/
|
||||
38
README.md
38
README.md
@@ -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}`.
|
||||
@@ -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),
|
||||
|
||||
69
index.js
69
index.js
@@ -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}`);
|
||||
});
|
||||
}
|
||||
@@ -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 },
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user