This commit is contained in:
2024-07-13 22:49:32 +02:00
18 changed files with 1609 additions and 39 deletions

View File

@@ -1,10 +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

3
.gitignore vendored
View File

@@ -137,3 +137,6 @@ logs/
# token
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.

980
database.json Normal file
View File

@@ -0,0 +1,980 @@
{
"tables": [
{
"name": "roles",
"columns": [
{
"name": "id",
"type": "INT UNSIGNED",
"constraints": [
"NOT NULL",
"AUTO_INCREMENT"
],
"primary_key": true
},
{
"name": "name",
"type": "VARCHAR(255)",
"constraints": [
"NOT NULL",
"UNIQUE"
]
},
{
"name": "user_bitfield",
"type": "INT UNSIGNED",
"constraints": [
"NOT NULL"
]
},
{
"name": "role_bitfield",
"type": "INT UNSIGNED",
"constraints": [
"NOT NULL"
]
},
{
"name": "verification_code_bitfield",
"type": "INT UNSIGNED",
"constraints": [
"NOT NULL"
]
},
{
"name": "ban_bitfield",
"type": "INT UNSIGNED",
"constraints": [
"NOT NULL"
]
},
{
"name": "patient_bitfield",
"type": "INT UNSIGNED",
"constraints": [
"NOT NULL"
]
},
{
"name": "doctor_bitfield",
"type": "INT UNSIGNED",
"constraints": [
"NOT NULL"
]
},
{
"name": "service_bitfield",
"type": "INT UNSIGNED",
"constraints": [
"NOT NULL"
]
},
{
"name": "company_bitfield",
"type": "INT UNSIGNED",
"constraints": [
"NOT NULL"
]
},
{
"name": "hospital_bitfield",
"type": "INT UNSIGNED",
"constraints": [
"NOT NULL"
]
},
{
"name": "room_bitfield",
"type": "INT UNSIGNED",
"constraints": [
"NOT NULL"
]
},
{
"name": "appointment_bitfield",
"type": "INT UNSIGNED",
"constraints": [
"NOT NULL"
]
}
],
"data": [
{
"name": "Admin",
"user_bitfield": 7,
"role_bitfield": 7,
"verification_code_bitfield": 7,
"ban_bitfield": 7,
"patient_bitfield": 7,
"doctor_bitfield": 7,
"service_bitfield": 7,
"company_bitfield": 7,
"hospital_bitfield": 7,
"room_bitfield": 7,
"appointment_bitfield": 7
},
{
"name": "Doctor",
"user_bitfield": 0,
"role_bitfield": 0,
"verification_code_bitfield": 0,
"ban_bitfield": 0,
"patient_bitfield": 1,
"doctor_bitfield": 1,
"service_bitfield": 1,
"company_bitfield": 1,
"hospital_bitfield": 1,
"room_bitfield": 1,
"appointment_bitfield": 0
},
{
"name": "Patient",
"user_bitfield": 0,
"role_bitfield": 0,
"verification_code_bitfield": 0,
"ban_bitfield": 0,
"patient_bitfield": 0,
"doctor_bitfield": 1,
"service_bitfield": 1,
"company_bitfield": 1,
"hospital_bitfield": 1,
"room_bitfield": 1,
"appointment_bitfield": 0
}
]
},
{
"name": "users",
"columns": [
{
"name": "id",
"type": "INT UNSIGNED",
"constraints": [
"NOT NULL",
"AUTO_INCREMENT"
],
"primary_key": true
},
{
"name": "first_name",
"type": "VARCHAR(64)",
"constraints": [
"NOT NULL"
]
},
{
"name": "last_name",
"type": "VARCHAR(64)",
"constraints": [
"NOT NULL"
]
},
{
"name": "username",
"type": "VARCHAR(64)",
"constraints": [
"NOT NULL"
]
},
{
"name": "password",
"type": "VARCHAR(255)",
"constraints": [
"NOT NULL"
]
},
{
"name": "email",
"type": "VARCHAR(128)",
"constraints": [
"NOT NULL"
]
},
{
"name": "email_verified",
"type": "BOOLEAN",
"constraints": [
"NOT NULL",
"DEFAULT FALSE"
]
},
{
"name": "phone",
"type": "VARCHAR(32)",
"constraints": [
"DEFAULT 'None'"
]
},
{
"name": "phone_verified",
"type": "BOOLEAN",
"constraints": [
"NOT NULL",
"DEFAULT FALSE"
]
}
]
},
{
"name": "user_roles",
"columns": [
{
"name": "user_id",
"type": "INT UNSIGNED",
"constraints": [
"NOT NULL"
]
},
{
"name": "role_id",
"type": "INT UNSIGNED",
"constraints": [
"NOT NULL"
]
}
],
"constraints": [
{
"primary_key": true,
"columns": ["user_id", "role_id"]
},
{
"foreign_key": true,
"name": "user_roles_user_id",
"column": "user_id",
"reference": "users(id)",
"on_delete": "RESTRICT",
"on_update": "CASCADE"
},
{
"foreign_key": true,
"name": "user_roles_role_id",
"column": "role_id",
"reference": "roles(id)",
"on_delete": "RESTRICT",
"on_update": "CASCADE"
},
{
"index": true,
"name": "user_roles_user_idx",
"columns": ["user_id"]
},
{
"index": true,
"name": "user_roles_role_idx",
"columns": ["role_id"]
}
]
},
{
"name": "verification_codes",
"columns": [
{
"name": "id",
"type": "INT UNSIGNED",
"constraints": [
"NOT NULL",
"AUTO_INCREMENT"
],
"primary_key": true
},
{
"name": "user_id",
"type": "INT UNSIGNED",
"constraints": [
"NOT NULL"
]
},
{
"name": "verification_code",
"type": "VARCHAR(255)",
"constraints": [
"NOT NULL"
]
},
{
"name": "type",
"type": "VARCHAR(32)",
"constraints": [
"NOT NULL"
]
},
{
"name": "created_at",
"type": "TIMESTAMP",
"constraints": [
"NOT NULL",
"DEFAULT CURRENT_TIMESTAMP"
]
}
],
"constraints": [
{
"foreign_key": true,
"name": "verification_codes_user_id",
"column": "user_id",
"reference": "users(id)",
"on_delete": "RESTRICT",
"on_update": "CASCADE"
},
{
"index": true,
"name": "verification_codes_user_idx",
"columns": ["user_id"]
}
]
},
{
"name": "bans",
"columns": [
{
"name": "id",
"type": "INT UNSIGNED",
"constraints": [
"NOT NULL",
"AUTO_INCREMENT"
],
"primary_key": true
},
{
"name": "user_id",
"type": "INT UNSIGNED",
"constraints": [
"NOT NULL"
]
},
{
"name": "reason",
"type": "TEXT",
"constraints": [
"NOT NULL"
]
},
{
"name": "created_at",
"type": "TIMESTAMP",
"constraints": [
"NOT NULL",
"DEFAULT CURRENT_TIMESTAMP"
]
}
],
"constraints": [
{
"foreign_key": true,
"name": "bans_user_id",
"column": "user_id",
"reference": "users(id)",
"on_delete": "RESTRICT",
"on_update": "CASCADE"
},
{
"index": true,
"name": "bans_user_idx",
"columns": ["user_id"]
}
]
},
{
"name": "doctors",
"columns": [
{
"name": "id",
"type": "INT UNSIGNED",
"constraints": [
"NOT NULL",
"AUTO_INCREMENT"
],
"primary_key": true
},
{
"name": "user_id",
"type": "INT UNSIGNED",
"constraints": [
"NOT NULL",
"UNIQUE"
]
},
{
"name": "email",
"type": "VARCHAR(255)",
"constraints": [
"NOT NULL"
]
},
{
"name": "phone",
"type": "VARCHAR(20)",
"constraints": [
"NOT NULL"
]
},
{
"name": "specialty",
"type": "VARCHAR(255)",
"constraints": [
"NOT NULL"
]
},
{
"name": "status",
"type": "ENUM('Available', 'Absent', 'Unavailable')",
"constraints": [
"NOT NULL",
"DEFAULT 'Available'"
]
},
{
"name": "is_verified",
"type": "BOOLEAN",
"constraints": [
"NOT NULL",
"DEFAULT FALSE"
]
}
],
"constraints": [
{
"foreign_key": true,
"name": "doctors_user_id",
"column": "user_id",
"reference": "users(id)",
"on_delete": "RESTRICT",
"on_update": "CASCADE"
},
{
"index": true,
"name": "doctors_user_idx",
"columns": ["user_id"]
}
]
},
{
"name": "patients",
"columns": [
{
"name": "id",
"type": "INT UNSIGNED",
"constraints": [
"NOT NULL",
"AUTO_INCREMENT"
],
"primary_key": true
},
{
"name": "user_id",
"type": "INT UNSIGNED",
"constraints": [
"NOT NULL",
"UNIQUE"
]
},
{
"name": "date_of_birth",
"type": "DATE",
"constraints": [
"NOT NULL"
]
},
{
"name": "gender",
"type": "ENUM('M', 'F', 'O')",
"constraints": [
"NOT NULL"
]
},
{
"name": "address",
"type": "VARCHAR(255)",
"constraints": [
"NOT NULL"
]
},
{
"name": "social_security_number",
"type": "VARCHAR(128)",
"constraints": [
"NOT NULL"
]
},
{
"name": "insurance_number",
"type": "VARCHAR(128)",
"constraints": [
"NOT NULL"
]
}
],
"constraints": [
{
"foreign_key": true,
"name": "patients_user_id",
"column": "user_id",
"reference": "users(id)",
"on_delete": "RESTRICT",
"on_update": "CASCADE"
},
{
"index": true,
"name": "patients_user_idx",
"columns": ["user_id"]
}
]
},
{
"name": "services",
"columns": [
{
"name": "id",
"type": "INT UNSIGNED",
"constraints": [
"NOT NULL",
"AUTO_INCREMENT"
],
"primary_key": true
},
{
"name": "name",
"type": "VARCHAR(255)",
"constraints": [
"NOT NULL"
]
},
{
"name": "description",
"type": "TEXT",
"constraints": [
"NOT NULL"
]
},
{
"name": "price",
"type": "DECIMAL(10, 2)",
"constraints": [
"NOT NULL"
]
}
]
},
{
"name": "service_doctors",
"columns": [
{
"name": "service_id",
"type": "INT UNSIGNED",
"constraints": [
"NOT NULL"
]
},
{
"name": "doctor_id",
"type": "INT UNSIGNED",
"constraints": [
"NOT NULL"
]
}
],
"constraints": [
{
"primary_key": true,
"columns": ["service_id", "doctor_id"]
},
{
"foreign_key": true,
"name": "service_doctors_service_id",
"column": "service_id",
"reference": "services(id)",
"on_delete": "RESTRICT",
"on_update": "CASCADE"
},
{
"foreign_key": true,
"name": "service_doctors_doctor_id",
"column": "doctor_id",
"reference": "doctors(id)",
"on_delete": "RESTRICT",
"on_update": "CASCADE"
},
{
"index": true,
"name": "service_doctors_service_idx",
"columns": ["service_id"]
},
{
"index": true,
"name": "service_doctors_doctor_idx",
"columns": ["doctor_id"]
}
]
},
{
"name": "companies",
"columns": [
{
"name": "id",
"type": "INT UNSIGNED",
"constraints": [
"NOT NULL",
"AUTO_INCREMENT"
],
"primary_key": true
},
{
"name": "name",
"type": "VARCHAR(255)",
"constraints": [
"NOT NULL"
]
},
{
"name": "code",
"type": "VARCHAR(2)",
"constraints": [
"NOT NULL"
]
},
{
"name": "logo",
"type": "VARCHAR(255)"
}
]
},
{
"name": "hospitals",
"columns": [
{
"name": "id",
"type": "INT UNSIGNED",
"constraints": [
"NOT NULL",
"AUTO_INCREMENT"
],
"primary_key": true
},
{
"name": "company_id",
"type": "INT UNSIGNED",
"constraints": [
"NOT NULL"
]
},
{
"name": "name",
"type": "VARCHAR(255)",
"constraints": [
"NOT NULL"
]
},
{
"name": "code",
"type": "VARCHAR(3)",
"constraints": [
"NOT NULL"
]
},
{
"name": "country",
"type": "VARCHAR(255)",
"constraints": [
"NOT NULL"
]
},
{
"name": "region",
"type": "VARCHAR(255)",
"constraints": [
"NOT NULL"
]
},
{
"name": "city",
"type": "VARCHAR(255)",
"constraints": [
"NOT NULL"
]
},
{
"name": "address",
"type": "VARCHAR(255)",
"constraints": [
"NOT NULL"
]
}
],
"constraints": [
{
"foreign_key": true,
"name": "hospitals_company_id",
"column": "company_id",
"reference": "companies(id)",
"on_delete": "RESTRICT",
"on_update": "CASCADE"
},
{
"index": true,
"name": "hospitals_company_idx",
"columns": ["company_id"]
}
]
},
{
"name": "rooms",
"columns": [
{
"name": "id",
"type": "INT UNSIGNED",
"constraints": [
"NOT NULL",
"AUTO_INCREMENT"
],
"primary_key": true
},
{
"name": "hospital_id",
"type": "INT UNSIGNED",
"constraints": [
"NOT NULL"
]
},
{
"name": "name",
"type": "VARCHAR(255)",
"constraints": [
"NOT NULL"
]
},
{
"name": "code",
"type": "VARCHAR(3)",
"constraints": [
"NOT NULL"
]
},
{
"name": "floor",
"type": "INT UNSIGNED",
"constraints": [
"NOT NULL"
]
},
{
"name": "room_number",
"type": "INT UNSIGNED",
"constraints": [
"NOT NULL"
]
},
{
"name": "room_type",
"type": "ENUM('General Ward', 'Private', 'Intensive Care Unit', 'Labor and Delivery', 'Operating', 'Recovery', 'Isolation', 'Emergency', 'Imaging', 'Procedure', 'Physical Therapy', 'Consultation')",
"constraints": [
"NOT NULL"
]
}
],
"constraints": [
{
"foreign_key": true,
"name": "rooms_hospital_id",
"column": "hospital_id",
"reference": "hospitals(id)",
"on_delete": "RESTRICT",
"on_update": "CASCADE"
},
{
"index": true,
"name": "rooms_hospital_idx",
"columns": ["hospital_id"]
}
]
},
{
"name": "hospital_doctors",
"columns": [
{
"name": "hospital_id",
"type": "INT UNSIGNED",
"constraints": [
"NOT NULL"
]
},
{
"name": "doctor_id",
"type": "INT UNSIGNED",
"constraints": [
"NOT NULL"
]
}
],
"constraints": [
{
"primary_key": true,
"columns": ["hospital_id", "doctor_id"]
},
{
"foreign_key": true,
"name": "hospital_doctors_hospital_id",
"column": "hospital_id",
"reference": "hospitals(id)",
"on_delete": "RESTRICT",
"on_update": "CASCADE"
},
{
"foreign_key": true,
"name": "hospital_doctors_doctor_id",
"column": "doctor_id",
"reference": "doctors(id)",
"on_delete": "RESTRICT",
"on_update": "CASCADE"
},
{
"index": true,
"name": "hospital_doctors_hospital_idx",
"columns": ["hospital_id"]
},
{
"index": true,
"name": "hospital_doctors_doctor_idx",
"columns": ["doctor_id"]
}
]
},
{
"name": "appointments",
"columns": [
{
"name": "id",
"type": "INT UNSIGNED",
"constraints": [
"NOT NULL",
"AUTO_INCREMENT"
],
"primary_key": true
},
{
"name": "patient_id",
"type": "INT UNSIGNED",
"constraints": [
"NOT NULL"
]
},
{
"name": "doctor_id",
"type": "INT UNSIGNED",
"constraints": [
"NOT NULL"
]
},
{
"name": "service_id",
"type": "INT UNSIGNED",
"constraints": [
"NOT NULL"
]
},
{
"name": "hospital_id",
"type": "INT UNSIGNED",
"constraints": [
"DEFAULT NULL"
]
},
{
"name": "room_id",
"type": "INT UNSIGNED",
"constraints": [
"DEFAULT NULL"
]
},
{
"name": "date",
"type": "DATE",
"constraints": [
"NOT NULL"
]
},
{
"name": "time",
"type": "TIME",
"constraints": [
"NOT NULL"
]
},
{
"name": "status",
"type": "ENUM('Confirmed', 'Completed', 'Absent', 'Cancelled by Patient', 'Cancelled by Doctor')",
"constraints": [
"NOT NULL"
]
}
],
"constraints": [
{
"foreign_key": true,
"name": "appointments_patient_id",
"column": "patient_id",
"reference": "patients(id)",
"on_delete": "RESTRICT",
"on_update": "CASCADE"
},
{
"foreign_key": true,
"name": "appointments_doctor_id",
"column": "doctor_id",
"reference": "doctors(id)",
"on_delete": "RESTRICT",
"on_update": "CASCADE"
},
{
"foreign_key": true,
"name": "appointments_service_id",
"column": "service_id",
"reference": "services(id)",
"on_delete": "RESTRICT",
"on_update": "CASCADE"
},
{
"foreign_key": true,
"name": "appointments_hospital_id",
"column": "hospital_id",
"reference": "hospitals(id)",
"on_delete": "RESTRICT",
"on_update": "CASCADE"
},
{
"foreign_key": true,
"name": "appointments_room_id",
"column": "room_id",
"reference": "rooms(id)",
"on_delete": "RESTRICT",
"on_update": "CASCADE"
},
{
"index": true,
"name": "appointments_patient_idx",
"columns": ["patient_id"]
},
{
"index": true,
"name": "appointments_doctor_idx",
"columns": ["doctor_id"]
},
{
"index": true,
"name": "appointments_service_idx",
"columns": ["service_id"]
},
{
"index": true,
"name": "appointments_hospital_idx",
"columns": ["hospital_id"]
},
{
"index": true,
"name": "appointments_room_idx",
"columns": ["room_id"]
}
]
}
]
}

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,17 +1,22 @@
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';
import rolesRouter from './routes/roles';
import doctorsRouter from './routes/doctors';
import patientsRouter from './routes/patients';
import servicesRouter from './routes/services';
import companiesRouter from './routes/companies';
import hospitalsRouter from './routes/hospitals';
@@ -20,33 +25,73 @@ 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'));
// routes
app.use('/api/test', testRouter);
app.use('/api/users', usersRouter);
app.use('/api/roles', rolesRouter);
app.use('/api/doctors', doctorsRouter);
app.use('/api/patients', patientsRouter);
app.use('/api/services', servicesRouter);
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

@@ -1,4 +1,6 @@
import mysql from 'mysql2';
import dbconf from '../database.json';
import { log } from './logManager';
const connection = mysql.createConnection({
host: process.env.DATABASE_HOST,
@@ -23,4 +25,58 @@ function createPool(host, user, password, db) {
return newPool;
}
export { connection, pool, createPool };
function databaseSelfTest() {
log('Database self-test');
dbconf.tables.forEach(table => {
let query = `CREATE TABLE IF NOT EXISTS ${table.name} (`;
table.columns.forEach((column, index) => {
query += `${column.name} ${column.type}`;
if (column.primary_key) query += ' PRIMARY KEY';
if (column.constraints && column.constraints.length > 0) query += ` ${column.constraints.join(' ')}`;
if (column.index) query += `, INDEX ${column.name}_idx (${column.name})`;
if (index < table.columns.length - 1) query += ', ';
});
if (table.constraints && table.constraints.length > 0) {
table.constraints.forEach(constraint => {
setTimeout(() => { // do not remove or it breaks /sarcasm
if (constraint.primary_key) query += `, PRIMARY KEY (${constraint.columns.join(', ')})`;
if (constraint.foreign_key) query += `, CONSTRAINT ${constraint.name} FOREIGN KEY (${constraint.column}) REFERENCES ${constraint.reference} ON DELETE ${constraint.on_delete} ON UPDATE ${constraint.on_update}`;
if (constraint.index) query += `, INDEX ${constraint.name} (${constraint.columns.join(', ')})`;
}, 500);
});
}
query += ') ENGINE=InnoDB;';
pool.query(query)
.then(() => log(`Table ${table.name} validated`))
.catch(err => console.log(`Error creating table ${table.name}: ${err}`));
if (table.data) {
pool.query(`SELECT * FROM ${table.name}`)
.then(([rows]) => {
if (rows.length === 0) {
table.data.forEach(row => {
let insertQuery = `INSERT INTO ${table.name} (`;
let values = 'VALUES (';
Object.keys(row).forEach((key, index) => {
insertQuery += key;
values += `'${row[key]}'`;
if (index < Object.keys(row).length - 1) {
insertQuery += ', ';
values += ', ';
}
});
insertQuery += ') ' + values + ');';
pool.query(insertQuery)
.then(() => log(`Row inserted in table ${table.name}`))
.catch(err => log(`Error inserting row in table ${table.name}: ${err}`));
});
}
})
.catch(err => log(`Error checking if table ${table.name} is empty: ${err}`));
}
});
}
databaseSelfTest();
export { connection, pool, createPool, databaseSelfTest };

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

@@ -47,6 +47,7 @@ export async function verifyPermissions(userId, permissionName, permissionType)
}
export async function checkIfUserEmailIsVerified(userId) {
if(process.env.DISABLE_EMAIL_VERIFICATION) return true;
try {
const [user] = await pool.execute('SELECT email_verified FROM users WHERE id = ? LIMIT 1', [userId]);
if (user.length === 0) return 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

@@ -63,7 +63,7 @@ router.post('/register', verifyToken, checkEmailVerified, checkBanned, async (re
if ([ email, phone, speciality, status ].every(Boolean)) {
try {
const [result] = await pool.execute(
'INSERT INTO doctors (user_id, email, phone, speciality, status) VALUES (?, ?, ?, ?, ?, ?)',
'INSERT INTO doctors (user_id, email, phone, speciality, status) VALUES (?, ?, ?, ?, ?)',
[req.userId, email, phone, speciality, status],
);
if (result.affectedRows === 0) return await respondWithStatus(res, 500, 'Error storing doctor');
@@ -83,10 +83,13 @@ router.post('/:doctorId/validate', verifyToken, checkBanned, checkPermissions('d
const { doctor_id } = req.body;
if (doctor_id) {
try {
const [result] = await pool.execute('UPDATE doctors SET is_verified = 1 WHERE id = ?', [doctor_id]);
if (result.affectedRows === 0) return await respondWithStatus(res, 500, 'Error validating doctor');
const [result2] = await pool.execute('INSERT INTO user_roles (user_id, role_id) VALUES (?, (SELECT id FROM roles WHERE name = ? LIMIT 1))', [req.userId, 'Doctor']);
if (result2.affectedRows === 0) return await respondWithStatus(res, 500, 'Error adding role to user');
const [result] = await pool.execute('SELECT * FROM doctors WHERE id = ?',[doctor_id]);
if(result.length === 0) return await respondWithStatus(res, 404, 'Doctor not found');
if(result[0].is_verified) return await respondWithStatus(res, 400, 'Doctor already verified');
const [result2] = await pool.execute('UPDATE doctors SET is_verified = 1 WHERE id = ?', [doctor_id]);
if (result2.affectedRows === 0) return await respondWithStatus(res, 500, 'Error validating doctor');
const [result3] = await pool.execute('INSERT INTO user_roles (user_id, role_id) VALUES (?, (SELECT id FROM roles WHERE name = ? LIMIT 1))', [result[0].user_id, 'Doctor']);
if (result3.affectedRows === 0) return await respondWithStatus(res, 500, 'Error adding role to user');
return await respondWithStatus(res, 200, 'Doctor validated successfully');
}
catch (err) {
@@ -102,6 +105,10 @@ router.post('/:doctorId/validate', verifyToken, checkBanned, checkPermissions('d
router.get('/:doctorId', verifyToken, checkBanned, async (req, res) => {
try {
const doctorId = await getDoctorId (req.userId);
if (req.params.doctorId == '@me') {
if (!doctorId) return await respondWithStatus(res, 404, 'Doctor not found');
req.params.doctorId = doctorId;
}
if (doctorId != req.params.doctorId && !verifyPermissions(req.userId, 'doctor', 2)) return await respondWithStatus(res, 403, 'Missing permission');
const [rows] = await pool.execute('SELECT * FROM doctors WHERE id = ? LIMIT 1', [req.params.doctorId]);
if (rows.length === 0) return await respondWithStatus(res, 404, 'Doctor not found');
@@ -117,6 +124,10 @@ router.patch('/:doctorId', verifyToken, checkBanned, async (req, res) => {
try {
const { type, value } = req.body;
const doctorId = await getDoctorId (req.userId);
if (req.params.doctorId == '@me') {
if (!doctorId) return await respondWithStatus(res, 404, 'Doctor not found');
req.params.doctorId = doctorId;
}
if (doctorId != req.params.doctorId && !verifyPermissions(req.userId, 'doctor', 2)) return await respondWithStatus(res, 403, 'Missing permission');
const [rows] = await pool.execute('SELECT * FROM doctors WHERE id = ? LIMIT 1', [req.params.doctorId]);
if (rows.length === 0) return await respondWithStatus(res, 404, 'Doctor not found');
@@ -143,6 +154,10 @@ router.put('/:doctorId', verifyToken, checkBanned, async (req, res) => {
if ([ user_id, email, phone, speciality, status, is_verified ].every(Boolean)) {
try {
const doctorId = await getDoctorId (req.userId);
if (req.params.doctorId == '@me') {
if (!doctorId) return await respondWithStatus(res, 404, 'Doctor not found');
req.params.doctorId = doctorId;
}
if (doctorId != req.params.doctorId && !verifyPermissions(req.userId, 'doctor', 2)) return await respondWithStatus(res, 403, 'Missing permission');
const [rows] = await pool.execute('SELECT * FROM doctors WHERE id = ? LIMIT 1', [req.params.doctorId]);
if (rows.length === 0) return await respondWithStatus(res, 404, 'Doctor not found');
@@ -168,6 +183,10 @@ router.put('/:doctorId', verifyToken, checkBanned, async (req, res) => {
router.delete('/:doctorId', verifyToken, checkBanned, async (req, res) => {
try {
const doctorId = await getDoctorId (req.userId);
if (req.params.doctorId == '@me') {
if (!doctorId) return await respondWithStatus(res, 404, 'Doctor not found');
req.params.doctorId = doctorId;
}
if (doctorId != req.params.doctorId && !verifyPermissions(req.userId, 'doctor', 4)) return await respondWithStatus(res, 403, 'Missing permission');
const [rows] = await pool.execute('SELECT * FROM doctors WHERE id = ? LIMIT 1', [req.params.doctorId]);
if (rows.length === 0) return await respondWithStatus(res, 404, 'Doctor not found');
@@ -186,6 +205,10 @@ router.delete('/:doctorId', verifyToken, checkBanned, async (req, res) => {
router.get('/:doctorId/appointments', verifyToken, checkBanned, async (req, res) => {
try {
const doctorId = await getDoctorId (req.userId);
if (req.params.doctorId == '@me') {
if (!doctorId) return await respondWithStatus(res, 404, 'Doctor not found');
req.params.doctorId = doctorId;
}
if (doctorId != req.params.doctorId && !verifyPermissions(req.userId, 'appointment', 1)) return await respondWithStatus(res, 403, 'Missing permission');
const [rows] = await pool.execute(
'SELECT a.*, u.first_name, u.last_name, p.gender, p.date_of_birth, s.service_name, a.service_id FROM appointments AS a JOIN patients AS p ON a.patient_id = p.id JOIN users AS u ON p.user_id = u.id JOIN services AS s ON a.service_id = s.id WHERE a.doctor_id = ?',
@@ -204,6 +227,10 @@ router.post('/:doctorId/appointments', verifyToken, checkBanned, async (req, res
const { patient_id, service_id, hospital_id, room_id = null, date, time, status } = req.body;
if (!['Confirmed', 'Completed', 'Absent', 'Cancelled by Patient', 'Cancelled by Doctor'].includes(status)) return await respondWithStatus(res, 400, 'Invalid status');
const doctorId = await getDoctorId (req.userId);
if (req.params.doctorId == '@me') {
if (!doctorId) return await respondWithStatus(res, 404, 'Doctor not found');
req.params.doctorId = doctorId;
}
if (doctorId != req.params.doctorId && !verifyPermissions(req.userId, 'appointment', 2)) return await respondWithStatus(res, 403, 'Missing permission');
if ([patient_id, service_id, hospital_id, date, time, status].every(Boolean)) {
try {
@@ -227,6 +254,10 @@ router.post('/:doctorId/appointments', verifyToken, checkBanned, async (req, res
router.get('/:doctorId/appointments/:appointmentId', verifyToken, checkBanned, async (req, res) => {
try {
const doctorId = await getDoctorId (req.userId);
if (req.params.doctorId == '@me') {
if (!doctorId) return await respondWithStatus(res, 404, 'Doctor not found');
req.params.doctorId = doctorId;
}
if (doctorId != req.params.doctorId && !verifyPermissions(req.userId, 'appointment', 1)) return await respondWithStatus(res, 403, 'Missing permission');
const [rows] = await pool.execute(
'SELECT a.*, u.first_name, u.last_name, p.gender, p.date_of_birth, s.service_name, a.service_id FROM appointments AS a JOIN patients AS p ON a.patient_id = p.id JOIN users AS u ON p.user_id = u.id WHERE a.id = ? AND a.doctor_id = ? LIMIT 1',
@@ -245,6 +276,10 @@ router.patch('/:doctorId/appointments/:appointmentId', verifyToken, checkBanned,
try {
const { type, value } = req.body;
const doctorId = await getDoctorId (req.userId);
if (req.params.doctorId == '@me') {
if (!doctorId) return await respondWithStatus(res, 404, 'Doctor not found');
req.params.doctorId = doctorId;
}
if (doctorId != req.params.doctorId && !verifyPermissions(req.userId, 'appointment', 2)) return await respondWithStatus(res, 403, 'Missing permission');
const [rows] = await pool.execute('SELECT * FROM appointments WHERE id = ? LIMIT 1', [req.params.appointmentId]);
if (rows.length === 0) return await respondWithStatus(res, 404, 'Appointment not found');
@@ -271,6 +306,10 @@ router.put('/:doctorId/appointments/:appointmentId', verifyToken, checkBanned, a
const { patient_id, service_id, hospital_id, room_id, date, time, status } = req.body;
if (!['Confirmed', 'Completed', 'Absent', 'Cancelled by Patient', 'Cancelled by Doctor'].includes(status)) return await respondWithStatus(res, 400, 'Invalid status');
const doctorId = await getDoctorId (req.userId);
if (req.params.doctorId == '@me') {
if (!doctorId) return await respondWithStatus(res, 404, 'Doctor not found');
req.params.doctorId = doctorId;
}
if (doctorId != req.params.doctorId && !verifyPermissions(req.userId, 'appointment', 2)) return await respondWithStatus(res, 403, 'Missing permission');
if ([patient_id, service_id, hospital_id, room_id, date, time, status].every(Boolean)) {
try {
@@ -297,6 +336,10 @@ router.put('/:doctorId/appointments/:appointmentId', verifyToken, checkBanned, a
router.delete('/:doctorId/appointments/:appointmentId', verifyToken, checkBanned, async (req, res) => {
try {
const doctorId = await getDoctorId (req.userId);
if (req.params.doctorId == '@me') {
if (!doctorId) return await respondWithStatus(res, 404, 'Doctor not found');
req.params.doctorId = doctorId;
}
if (doctorId != req.params.doctorId && !verifyPermissions(req.userId, 'appointment', 4)) return await respondWithStatus(res, 403, 'Missing permission');
const [rows] = await pool.execute('SELECT * FROM appointments WHERE id = ? LIMIT 1', [req.params.appointmentId]);
if (rows.length === 0) return await respondWithStatus(res, 404, 'Appointment not found');
@@ -315,6 +358,10 @@ router.delete('/:doctorId/appointments/:appointmentId', verifyToken, checkBanned
router.get('/:doctorId/services', verifyToken, checkBanned, async (req, res) => {
try {
const doctorId = await getDoctorId (req.userId);
if (req.params.doctorId == '@me') {
if (!doctorId) return await respondWithStatus(res, 404, 'Doctor not found');
req.params.doctorId = doctorId;
}
if (doctorId != req.params.doctorId && !verifyPermissions(req.userId, 'service', 1)) return await respondWithStatus(res, 403, 'Missing permission');
const [rows] = await pool.execute('SELECT s.* FROM services s INNER JOIN service_doctors sd ON s.id = sd.service_id WHERE sd.doctor_id = ?', [req.params.doctorId]);
if (rows.length === 0) return await respondWithStatus(res, 404, 'Services not found');
@@ -329,6 +376,10 @@ router.get('/:doctorId/services', verifyToken, checkBanned, async (req, res) =>
router.post('/:doctorId/services', verifyToken, checkBanned, async (req, res) => {
const { service_id } = req.body;
const doctorId = await getDoctorId (req.userId);
if (req.params.doctorId == '@me') {
if (!doctorId) return await respondWithStatus(res, 404, 'Doctor not found');
req.params.doctorId = doctorId;
}
if (doctorId != req.params.doctorId && !verifyPermissions(req.userId, 'service', 2)) return await respondWithStatus(res, 403, 'Missing permission');
if (service_id) {
try {
@@ -349,6 +400,10 @@ router.post('/:doctorId/services', verifyToken, checkBanned, async (req, res) =>
router.patch('/:doctorId/services/:serviceId', verifyToken, checkBanned, async (req, res) => {
const { type, value } = req.body;
const doctorId = await getDoctorId (req.userId);
if (req.params.doctorId == '@me') {
if (!doctorId) return await respondWithStatus(res, 404, 'Doctor not found');
req.params.doctorId = doctorId;
}
if (doctorId != req.params.doctorId && !verifyPermissions(req.userId, 'service', 2)) return await respondWithStatus(res, 403, 'Missing permission');
if (type === 'service_id') {
try {
@@ -368,6 +423,10 @@ router.patch('/:doctorId/services/:serviceId', verifyToken, checkBanned, async (
router.delete('/:doctorId/services/:serviceId', verifyToken, checkBanned, async (req, res) => {
const doctorId = await getDoctorId (req.userId);
if (req.params.doctorId == '@me') {
if (!doctorId) return await respondWithStatus(res, 404, 'Doctor not found');
req.params.doctorId = doctorId;
}
if (doctorId != req.params.doctorId && !verifyPermissions(req.userId, 'service', 4)) return await respondWithStatus(res, 403, 'Missing permission');
try {
const [result] = await pool.execute('DELETE FROM service_doctors WHERE doctor_id = ? AND service_id = ?', [req.params.doctorId, req.params.serviceId]);
@@ -380,6 +439,25 @@ router.delete('/:doctorId/services/:serviceId', verifyToken, checkBanned, async
}
});
router.get('/:doctorId/hospitals', verifyToken, checkBanned, async (req,res) => {
const doctorId = await getDoctorId(req.userId);
if (req.params.doctorId == '@me') {
if (!doctorId) return await respondWithStatus(res, 404, 'Doctor not found');
req.params.doctorId = doctorId;
}
if (doctorId != req.params.doctorId && !verifyPermissions(req.userId, 'service', 4)) return await respondWithStatus(res, 403, 'Missing permission');
try {
//'SELECT s.* FROM services s INNER JOIN service_doctors sd ON s.id = sd.service_id WHERE sd.doctor_id = ?', [req.params.doctorId]
const [rows] = await pool.execute('SELECT h.* FROM hospitals h INNER JOIN hospital_doctors hd ON h.id = hd.hospital_id WHERE hd.doctor_id = ?', [req.params.doctorId]);
if (rows.length === 0) return await respondWithStatus(res, 404, 'Hospitals not found');
return await respondWithStatusJSON(res, 200, rows);
}
catch (err) {
error(err);
return await respondWithStatus(res, 500, 'An error has occured');
}
})
export default router;

View File

@@ -85,7 +85,7 @@ router.put('/:hospitalId', verifyToken, checkBanned, checkPermissions('hospital'
return await respondWithStatus(res, 404, 'Hospital not found');
}
const [result] = await pool.execute(
'UPDATE hospitals SET company_id = ?, name = ?, country = ?, region = ?, city = ?, address = ? WHERE id = ?',
'UPDATE hospitals SET company_id = ?, name = ?, code = ?, country = ?, region = ?, city = ?, address = ? WHERE id = ?',
[company_id, name, code, country, region, city, address, id],
);

View File

@@ -83,6 +83,10 @@ router.post('/register', verifyToken, checkEmailVerified, checkBanned, async (re
router.get('/:patientId', verifyToken, checkBanned, async (req, res) => {
try {
const patientId = await getPatientId(req.userId);
if (req.params.patientId == '@me') {
if (!patientId) return await respondWithStatus(res, 404, 'Patient not found');
req.params.patientId = patientId;
}
if (patientId != req.params.patientId && !verifyPermissions(req.userId, 'patient', 1)) return await respondWithStatus(res, 403, 'Missing permission');
const [rows] = await pool.execute('SELECT * FROM patients WHERE id = ? LIMIT 1', [req.params.patientId]);
if (rows.length === 0) return await respondWithStatus(res, 404, 'Patient not found');
@@ -98,6 +102,10 @@ router.patch('/:patientId', verifyToken, checkBanned, async (req, res) => {
try {
const { type, value } = req.body;
const patientId = await getPatientId(req.userId);
if (req.params.patientId == '@me') {
if (!patientId) return await respondWithStatus(res, 404, 'Patient not found');
req.params.patientId = patientId;
}
if (patientId != req.params.patientId && !verifyPermissions(req.userId, 'patient', 2)) return await respondWithStatus(res, 403, 'Missing permission');
const [rows] = await pool.execute('SELECT * FROM patients WHERE id = ? LIMIT 1', [req.params.patientId]);
if (rows.length === 0) return await respondWithStatus(res, 404, 'Patient not found');
@@ -124,6 +132,10 @@ router.put('/:patientId', verifyToken, checkBanned, async (req, res) => {
if ([ user_id, date_of_birth, gender, address, social_security_number, insurance_number ].every(Boolean)) {
try {
const patientId = await getPatientId(req.userId);
if (req.params.patientId == '@me') {
if (!patientId) return await respondWithStatus(res, 404, 'Patient not found');
req.params.patientId = patientId;
}
if (patientId != req.params.patientId && !verifyPermissions(req.userId, 'patient', 2)) return await respondWithStatus(res, 403, 'Missing permission');
const [rows] = await pool.execute('SELECT * FROM patients WHERE id = ? LIMIT 1', [req.params.patientId]);
if (rows.length === 0) return await respondWithStatus(res, 404, 'Patient not found');
@@ -149,6 +161,10 @@ router.put('/:patientId', verifyToken, checkBanned, async (req, res) => {
router.delete('/:patientId', verifyToken, checkBanned, async (req, res) => {
try {
const patientId = await getPatientId(req.userId);
if (req.params.patientId == '@me') {
if (!patientId) return await respondWithStatus(res, 404, 'Patient not found');
req.params.patientId = patientId;
}
if (patientId != req.params.patientId && !verifyPermissions(req.userId, 'patient', 4)) return await respondWithStatus(res, 403, 'Missing permission');
const [rows] = await pool.execute('SELECT * FROM patients WHERE id = ? LIMIT 1', [req.params.patientId]);
if (rows.length === 0) return await respondWithStatus(res, 404, 'Patient not found');
@@ -167,6 +183,10 @@ router.delete('/:patientId', verifyToken, checkBanned, async (req, res) => {
router.get('/:patientId/appointments', verifyToken, checkBanned, async (req, res) => {
try {
const patientId = await getPatientId(req.userId);
if (req.params.patientId == '@me') {
if (!patientId) return await respondWithStatus(res, 404, 'Patient not found');
req.params.patientId = patientId;
}
if (patientId != req.params.patientId && !verifyPermissions(req.userId, 'appointment', 1)) return await respondWithStatus(res, 403, 'Missing permission');
const [rows] = await pool.execute(
'SELECT a.id, u.first_name, u.last_name, d.email, d.phone, h.name, h.address, a.date, a.time, a.status, s.name FROM appointments a JOIN doctors d ON a.doctor_id = d.id JOIN users u ON d.user_id = u.id JOIN hospitals h ON a.hospital_id = h.id JOIN services s ON a.service_id = s.id WHERE a.patient_id = ?',
@@ -186,6 +206,10 @@ router.post('/:patientId/appointments', verifyToken, checkBanned, async (req, re
if ([ doctor_id, service_id, hospital_id, date, time ].every(Boolean)) {
try {
const patientId = await getPatientId(req.userId);
if (req.params.patientId == '@me') {
if (!patientId) return await respondWithStatus(res, 404, 'Patient not found');
req.params.patientId = patientId;
}
if (patientId != req.params.patientId && !verifyPermissions(req.userId, 'appointment', 2)) return await respondWithStatus(res, 403, 'Missing permission');
const [result] = await pool.execute(
'INSERT INTO appointments (doctor_id, service_id, hospital_id, patient_id, date, time) VALUES (?, ?, ?, ?, ?, ?)',
@@ -203,10 +227,54 @@ router.post('/:patientId/appointments', verifyToken, checkBanned, async (req, re
return await respondWithStatus(res, 400, 'Missing fields');
}
});
// GET /:patientId/appointments/:appointmentId
router.get('/:patientId/appointments/:appointmentId', verifyToken, checkBanned, async (req, res) => {
try {
const patientId = await getPatientId(req.userId);
if (req.params.patientId == '@me') {
if (!patientId) return await respondWithStatus(res, 404, 'Patient not found');
req.params.patientId = patientId;
}
if (patientId != req.params.patientId && !verifyPermissions(req.userId, 'appointment', 1)) return await respondWithStatus(res, 403, 'Missing permission');
const [rows] = await pool.execute(
'SELECT a.id, u.first_name, u.last_name, d.email, d.phone, h.name, h.address, a.date, a.time, a.status, s.name FROM appointments a JOIN doctors d ON a.doctor_id = d.id JOIN users u ON d.user_id = u.id JOIN hospitals h ON a.hospital_id = h.id JOIN services s ON a.service_id = s.id WHERE a.id = ? AND a.patient_id = ?',
[req.params.appointmentId, req.params.patientId],
);
if (rows.length === 0) return await respondWithStatus(res, 404, 'Appointment not found');
return await respondWithStatusJSON(res, 200, rows[0]);
}
catch (err) {
error(err);
return await respondWithStatus(res, 500, 'An error has occured');
}
});
// PATCH /:patientId/appointments/:appointmentId
// PUT /:patientId/appointments/:appointmentId
// DELETE /:patientId/appointments/:appointmentId
router.delete('/:patientId/appointments/:appointmentId', verifyToken, checkBanned, async (req, res) => {
try {
const patientId = await getPatientId(req.userId);
if (req.params.patientId == '@me') {
if (!patientId) return await respondWithStatus(res, 404, 'Patient not found');
req.params.patientId = patientId;
}
if (patientId != req.params.patientId && !verifyPermissions(req.userId, 'appointment', 4)) return await respondWithStatus(res, 403, 'Missing permission');
const [rows] = await pool.execute('SELECT * FROM appointments WHERE id = ? AND patient_id = ? LIMIT 1', [req.params.appointmentId, req.params.patientId]);
if (rows.length === 0) return await respondWithStatus(res, 404, 'Appointment not found');
const [result] = await pool.execute('DELETE FROM appointments WHERE id = ?', [req.params.appointmentId]);
if (result.affectedRows === 0) return await respondWithStatus(res, 500, 'Error removing appointment');
return await respondWithStatus(res, 200, 'Appointment deleted successfully');
}
catch (err) {
error(err);
return await respondWithStatus(res, 500, 'An error has occured');
}
});
export default router;

92
routes/roles.js Normal file
View File

@@ -0,0 +1,92 @@
import express from 'express';
import { error } from '../modules/logManager';
import { pool } from '../modules/databaseManager';
import { verifyToken } from '../modules/tokenManager';
import { respondWithStatus, respondWithStatusJSON } from '../modules/requestHandler';
import { checkBanned, checkPermissions } from '../modules/permissionManager';
const router = express.Router();
// GET role list
router.get('/', verifyToken, checkBanned, checkPermissions('role', 1), async (req, res) => {
try {
const rows = await pool.execute('SELECT * FROM roles');
if (rows[0].length === 0) return await respondWithStatus(res, 404, 'No roles found');
return await respondWithStatusJSON(res, rows[0]);
}
catch (err) {
error(err);
return await respondWithStatus(res, 500, 'An error has occured');
}
});
// POST create role
router.post('/', verifyToken, checkBanned, checkPermissions('role', 2), async (req, res) => {
const { name, user_bitfield, role_bitfield, verification_code_bitfield, ban_bitfield, patient_bitfield, doctor_bitfield, service_bitfield, company_bitfield, hospital_bitfield, room_bitfield, appointment_bitfield } = req.body;
if ([ name, user_bitfield, role_bitfield, verification_code_bitfield, ban_bitfield, patient_bitfield, doctor_bitfield, service_bitfield, company_bitfield, hospital_bitfield, room_bitfield, appointment_bitfield ].every(Boolean)) {
try {
await pool.execute(
'INSERT INTO users (name, user_bitfield, role_bitfield, verification_code_bitfield, ban_bitfield, patient_bitfield, doctor_bitfield, service_bitfield, company_bitfield, hospital_bitfield, room_bitfield, appointment_bitfield) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
[ name, user_bitfield, role_bitfield, verification_code_bitfield, ban_bitfield, patient_bitfield, doctor_bitfield, service_bitfield, company_bitfield, hospital_bitfield, room_bitfield, appointment_bitfield ],
);
return await respondWithStatus(res, 200, 'Role created successfully');
}
catch (err) {
error(err);
return await respondWithStatus(res, 500, 'An error has occured');
}
}
else {
return await respondWithStatus(res, 400, 'Missing fields');
}
});
// GET role
router.get('/:id', verifyToken, checkBanned, checkPermissions('role', 1), async (req, res) => {
try {
const [rows] = await pool.execute('SELECT * FROM roles WHERE id = ? LIMIT 1', [ req.params.id ]);
if (rows.length === 0) return await respondWithStatus(res, 404, 'Role not found');
return await respondWithStatusJSON(res, rows[0]);
}
catch (err) {
error(err);
return await respondWithStatus(res, 500, 'An error has occured');
}
});
// PUT update role
router.put('/:id', verifyToken, checkBanned, checkPermissions('role', 2), async (req, res) => {
const { name, user_bitfield, role_bitfield, verification_code_bitfield, ban_bitfield, patient_bitfield, doctor_bitfield, service_bitfield, company_bitfield, hospital_bitfield, room_bitfield, appointment_bitfield } = req.body;
if ([ name, user_bitfield, role_bitfield, verification_code_bitfield, ban_bitfield, patient_bitfield, doctor_bitfield, service_bitfield, company_bitfield, hospital_bitfield, room_bitfield, appointment_bitfield ].every(Boolean)) {
try {
await pool.execute(
'UPDATE roles SET name = ?, user_bitfield = ?, role_bitfield = ?, verification_code_bitfield = ?, ban_bitfield = ?, patient_bitfield = ?, doctor_bitfield = ?, service_bitfield = ?, company_bitfield = ?, hospital_bitfield = ?, room_bitfield = ?, appointment_bitfield = ? WHERE id = ?',
[ name, user_bitfield, role_bitfield, verification_code_bitfield, ban_bitfield, patient_bitfield, doctor_bitfield, service_bitfield, company_bitfield, hospital_bitfield, room_bitfield, appointment_bitfield, req.params.id ],
);
return await respondWithStatus(res, 200, 'Role updated successfully');
}
catch (err) {
error(err);
return await respondWithStatus(res, 500, 'An error has occured');
}
}
else {
return await respondWithStatus(res, 400, 'Missing fields');
}
});
// DELETE role
router.delete('/:id', verifyToken, checkBanned, checkPermissions('role', 4), async (req, res) => {
try {
await pool.execute('DELETE FROM roles WHERE id = ?', [ req.params.id ]);
return await respondWithStatus(res, 200, 'Role deleted successfully');
}
catch (err) {
error(err);
return await respondWithStatus(res, 500, 'An error has occured');
}
});
export default router;

122
routes/services.js Normal file
View File

@@ -0,0 +1,122 @@
import express from 'express';
import { error } from '../modules/logManager';
import { pool } from '../modules/databaseManager';
import { verifyToken } from '../modules/tokenManager';
import { checkPermissions, checkBanned } from '../modules/permissionManager';
import { respondWithStatus, respondWithStatusJSON } from '../modules/requestHandler';
const router = express.Router();
router.get('/', verifyToken, checkBanned, checkPermissions('service', 1), async (req, res) => {
try {
const [rows] = await pool.execute('SELECT * FROM services WHERE 1');
if (rows.length === 0) return await respondWithStatus(res, 404, 'Services not found');
return await respondWithStatusJSON(res, 200, rows);
}
catch (err) {
error(err);
return await respondWithStatus(res, 500, 'An error has occured');
}
});
router.post('/', verifyToken, checkBanned, checkPermissions('service', 2), async (req, res) => {
const { name, description, price } = req.body;
if ([ name, description, price ].every(Boolean)) {
try {
const [result] = await pool.execute(
'INSERT INTO services (name, description, price) VALUES (?, ?, ?)',
[ name, description, price ],
);
if (result.affectedRows === 0) return await respondWithStatus(res, 500, 'Error storing service');
return await respondWithStatus(res, 200, 'Service created successfully');
}
catch (err) {
error(err);
return await respondWithStatus(res, 500, 'An error has occured');
}
}
else {
return await respondWithStatus(res, 400, 'Missing fields');
}
});
router.get('/:serviceId', verifyToken, checkBanned, checkPermissions('service', 1), async (req, res) => {
try {
const [rows] = await pool.execute('SELECT * FROM services WHERE id = ? LIMIT 1', [req.params.serviceId]);
if (rows.length === 0) return await respondWithStatus(res, 404, 'Services not found');
return await respondWithStatusJSON(res, 200, rows[0]);
}
catch (err) {
error(err);
return await respondWithStatus(res, 500, 'An error has occured');
}
});
router.patch('/:serviceId', verifyToken, checkBanned, checkPermissions('service', 2), async (req, res) => {
try {
const { type, value } = req.body;
const [rows] = await pool.execute('SELECT * FROM services WHERE id = ? LIMIT 1', [req.params.serviceId]);
if (rows.length === 0) return await respondWithStatus(res, 404, 'Service not found');
const fields = rows.map(row => Object.keys(row));
if (fields[0].includes(type)) {
const [result] = await pool.execute(`UPDATE services SET ${type} = ? WHERE id = ?`, [value, req.params.serviceId]);
if (result.affectedRows === 0) return await respondWithStatus(res, 500, 'Error updating service');
return await respondWithStatus(res, 200, 'Service updated successfully');
}
else {
return await respondWithStatus(res, 400, 'Invalid type');
}
}
catch (err) {
error(err);
return await respondWithStatus(res, 500, 'An error has occured');
}
});
router.put('/:serviceId', verifyToken, checkBanned, checkPermissions('service', 2), async (req, res) => {
const id = req.params.serviceId;
const { name, description, price } = req.body;
if ([ name, description, price ].every(Boolean)) {
try {
const [rows] = await pool.execute('SELECT * FROM services WHERE id = ? LIMIT 1', [id]);
if (rows.length === 0) {
return await respondWithStatus(res, 404, 'Service not found');
}
const [result] = await pool.execute(
'UPDATE services SET name = ?, description = ?, price = ? WHERE id = ?',
[name, description, price, id],
);
if (result.affectedRows === 0) {
return await respondWithStatus(res, 500, 'Error updating Service');
}
return await respondWithStatus(res, 200, 'Service updated successfully');
}
catch (err) {
error(err);
return await respondWithStatus(res, 500, 'An error has occured');
}
}
else {
return await respondWithStatus(res, 400, 'Missing fields');
}
});
router.delete('/:serviceId', verifyToken, checkBanned, checkPermissions('service', 4), async (req, res) => {
try {
const [rows] = await pool.execute('SELECT * FROM services WHERE id = ? LIMIT 1', [req.params.serviceId]);
if (rows.length === 0) return await respondWithStatus(res, 404, 'service not found');
const [result] = await pool.execute('DELETE FROM services WHERE id = ?', [req.params.serviceId]);
if (result.affectedRows === 0) return await respondWithStatus(res, 500, 'Error removing Service');
return await respondWithStatus(res, 200, 'Service deleted successfully');
}
catch (err) {
error(err);
return await respondWithStatus(res, 500, 'An error has occured');
}
});
export default router;

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,6 +30,8 @@ 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 == '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');
pool.execute('INSERT INTO verification_codes (user_id, verification_code, type) VALUES (?, ?, ?)', [ rows[0].id, code, 'email' ]);
@@ -49,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 {
@@ -74,6 +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' ? true : user.email_verified,
},
});
}
@@ -121,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]);
@@ -138,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) {
@@ -170,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 {
@@ -201,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 {
@@ -241,6 +244,7 @@ router.patch('/password/verify', async (req, res) => {
router.get('/:userId', verifyToken, checkBanned, async (req, res) => {
try {
if (req.params.userId == '@me') req.params.userId = req.userId;
if (req.params.userId != req.userId && !verifyPermissions(req.userId, 'user', 1)) return await respondWithStatus(res, 403, 'Missing permission');
const [rows] = await pool.execute('SELECT id, first_name, last_name, username, email, phone FROM users WHERE id = ? LIMIT 1', [req.params.userId]);
if (rows.length === 0) return await respondWithStatus(res, 404, 'User not found');
@@ -257,6 +261,7 @@ router.get('/:userId', verifyToken, checkBanned, async (req, res) => {
router.patch('/:userId', verifyToken, checkBanned, async (req, res) => {
try {
if (req.params.userId == '@me') req.params.userId = req.userId;
if (req.params.userId != req.userId && !verifyPermissions(req.userId, 'user', 2)) return await respondWithStatus(res, 403, 'Missing permission');
const { type } = req.body;
let { value } = req.body;
@@ -282,6 +287,7 @@ router.patch('/:userId', verifyToken, checkBanned, async (req, res) => {
router.put('/:userId', verifyToken, checkBanned, async (req, res) => {
try {
if (req.params.userId == '@me') req.params.userId = req.userId;
if (req.params.userId != req.userId && !verifyPermissions(req.userId, 'user', 2)) return await respondWithStatus(res, 403, 'Missing permission');
const { first_name, last_name, username, password = null, email, phone = null } = req.body;
if ([first_name, last_name, username, email].every(Boolean)) {
@@ -310,6 +316,7 @@ router.put('/:userId', verifyToken, checkBanned, async (req, res) => {
router.delete('/:userId', verifyToken, checkBanned, async (req, res) => {
try {
if (req.params.userId == '@me') req.params.userId = req.userId;
if (req.params.userId != req.userId && !verifyPermissions(req.userId, 'user', 4)) return await respondWithStatus(res, 403, 'Missing permission');
if (!userExists(req.params.userId)) return await respondWithStatus(res, 404, 'User not found');
const [result] = await pool.execute('DELETE FROM users WHERE id = ?', [ req.params.userId ]);
@@ -322,4 +329,52 @@ router.delete('/:userId', verifyToken, checkBanned, async (req, res) => {
}
});
router.get('/:userId/roles', verifyToken, checkBanned, async (req, res) => {
try {
if (req.params.userId == '@me') req.params.userId = req.userId;
if (req.params.userId != req.userId && !verifyPermissions(req.userId, 'user', 1)) return await respondWithStatus(res, 403, 'Missing permission');
const [rows] = await pool.execute('SELECT r.* 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 = ?', [ req.params.userId ]);
if (rows.length === 0) return await respondWithStatus(res, 404, 'No roles found');
return await respondWithStatusJSON(res, 200, rows);
}
catch (err) {
error(err);
return await respondWithStatus(res, 500, 'An error has occured');
}
});
router.post('/:userId/roles', verifyToken, checkBanned, checkPermissions('user', 2), async (req, res) => {
const { roleId } = req.body;
if (roleId) {
try {
if (req.params.userId == '@me') req.params.userId = req.userId;
const [rows] = await pool.execute('SELECT * FROM roles WHERE id = ? LIMIT 1', [ roleId ]);
if (rows.length === 0) return await respondWithStatus(res, 404, 'Role not found');
const [result] = await pool.execute('INSERT INTO user_roles (user_id, role_id) VALUES (?, ?)', [ req.params.userId, roleId ]);
if (result.affectedRows === 0) return await respondWithStatus(res, 500, 'Error assigning role');
return await respondWithStatus(res, 200, 'Role assigned successfully');
}
catch (err) {
error(err);
return await respondWithStatus(res, 500, 'An error has occured');
}
}
else {
return await respondWithStatus(res, 400, 'Missing fields');
}
});
router.delete('/:userId/roles/:roleId', verifyToken, checkBanned, checkPermissions('user', 4), async (req, res) => {
try {
if (req.params.userId == '@me') req.params.userId = req.userId;
const [result] = await pool.execute('DELETE FROM user_roles WHERE user_id = ? AND role_id = ?', [ req.params.userId, req.params.roleId ]);
if (result.affectedRows === 0) return await respondWithStatus(res, 500, 'Error removing role');
return await respondWithStatus(res, 200, 'Role removed successfully');
}
catch (err) {
error(err);
return await respondWithStatus(res, 500, 'An error has occured');
}
});
export default router;