From 4fbc9819e8d5d160ab99256437bea9ff002726d1 Mon Sep 17 00:00:00 2001 From: Lightemerald Date: Sun, 24 Mar 2024 11:14:03 +0100 Subject: [PATCH 1/6] Role update - Added verify status to user login - Added option to disable email verification - Added roles route - Added role management to users --- .env.sample | 3 +- bun.lockb | Bin 97275 -> 97275 bytes index.js | 2 ++ routes/roles.js | 92 ++++++++++++++++++++++++++++++++++++++++++++++++ routes/users.js | 48 +++++++++++++++++++++++++ 5 files changed, 144 insertions(+), 1 deletion(-) create mode 100644 routes/roles.js diff --git a/.env.sample b/.env.sample index 8d2906f..81e198c 100644 --- a/.env.sample +++ b/.env.sample @@ -7,4 +7,5 @@ DATABASE_PASSWORD="" JWT_SECRET="" SMTP= MAIL= -MAIL_PASS= \ No newline at end of file +MAIL_PASS= +DISABLE_EMAIL_VERIFICATION=true diff --git a/bun.lockb b/bun.lockb index 6bed908634736a42adf59a9a11cdcabf6d3214bd..0a0b0fde2b542566fe2108da16b40267400358cf 100755 GIT binary patch delta 1564 zcmezUo%Q#3)(uk=85t)}OAKdZocs<6~9+*4^Cgn0fB4uFm9GFzg1c}sv$!lOzFAF5n1}5); zNwaK_NFSJd1}5!tKqAw? z`oQEfFlko~5}5`j-+@WD3XsS=F!>Ek`c;BNmVwECU^1)lFabR*AnB=Pg ziKKzaePB|o79^4fCXazhxjK+Y8JIi=Ce`XeB6VQ$8kp2;0Ex7L$$MbZtPv#A2PU6^ zNxLSH$TTqd4otc=gGABj+Vz4& zrh&;U~(Ino8JPSBCc|cfMAm^xwmBen9GKh&Ci&)qMAE?IJ}@aZ4NW{W@~ePHq#n6z6A5}5`j-+@WD zB_NS`VDcN7^jiuNSq3KmfyuCCAdz)ol5IJN9S0`2fl0mT&m}J`oV#k5WZD5jbGe{&2Ozs1dVp~8Wd0_Gwn3UTJ z5-9_d=fI@eHjqdin7jri^|pgV+Q8&JFln{}B+>^apMgocogk5EJHsb05D=gIK!9a4 I$F9!@0WmoU-~a#s delta 1566 zcmezUo%Q#3)(uk=85t%|OAKd>oBR$;x+Q_w^T6acFzJ^J5?KZ&|AEP{6p+X|Fv*q* zV#k5WZD5iw4J48VCij6!v2>6~9+*4^Cgn0fB4uFm9GFzg1c}sv$!lOzFAF5n1}5); zNwaK_NFSJd1}5!tKqAw? z`oQEfFlko~5}5`j-+@WD3XsS=F!>Ek`c;BNmVwECU^1)lFabR*AnB=Pg ziKKzaePB|o79^4fCXazhxjK+Y8JIi=Ce`XeB6VQ$8kp2;0Ex7L$$MbZtPv#A2PU6^ zNxLSH$TTqd4otc=gGABj+Vz4& zrh&;U~(Ino8JPSBCc|cfMAm^xwmBen9GKh&Ci&)qMAE?IJ}@aZ4NW{W@~ePHq#n6z6A5}5`j-+@WD zB_NS`VDcN7^jiuNSq3KmfyuCCAdz)ol5IJN9S0`2fl0mT&m}J`oV#k5WZD5jbGe{&2Ozs1dVp~8Wd0_Gwn3UTJ z5-9_d=fI@eHjqdin7jri^|pgV+Q8&JFln{}B+>^apMgocogk5EU~-{=*yINSESouY HeLe^PbGc?d diff --git a/index.js b/index.js index 7aa88fe..f7faff3 100644 --- a/index.js +++ b/index.js @@ -10,6 +10,7 @@ import { speedLimiter, checkSystemLoad } 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 companiesRouter from './routes/companies'; @@ -37,6 +38,7 @@ 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/companies', companiesRouter); diff --git a/routes/roles.js b/routes/roles.js new file mode 100644 index 0000000..2df91a4 --- /dev/null +++ b/routes/roles.js @@ -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; \ No newline at end of file diff --git a/routes/users.js b/routes/users.js index b7f863e..c0a2dc1 100644 --- a/routes/users.js +++ b/routes/users.js @@ -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) 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' ]); @@ -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 : user.email_verified, }, }); } @@ -322,4 +325,49 @@ router.delete('/:userId', verifyToken, checkBanned, async (req, res) => { } }); +router.get('/:userId/roles', verifyToken, checkBanned, async (req, res) => { + try { + 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, 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 { + 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 { + 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; \ No newline at end of file From abd6f6747f6db7abb0d3c9c59f03a0cf21fc0c5d Mon Sep 17 00:00:00 2001 From: Lightemerald Date: Sun, 24 Mar 2024 11:40:35 +0100 Subject: [PATCH 2/6] Added @me endpoint support --- routes/doctors.js | 56 ++++++++++++++++++++++++++++++++++++++ routes/patients.js | 68 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 124 insertions(+) diff --git a/routes/doctors.js b/routes/doctors.js index 183d8b0..d2ef380 100644 --- a/routes/doctors.js +++ b/routes/doctors.js @@ -102,6 +102,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 +121,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 +151,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 +180,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 +202,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 +224,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 +251,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 +273,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 +303,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 +333,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 +355,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 +373,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 +397,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 +420,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]); diff --git a/routes/patients.js b/routes/patients.js index ab44f40..7f77f0a 100644 --- a/routes/patients.js +++ b/routes/patients.js @@ -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; From d93bfe333dee3b0964403a720074072823c85d3a Mon Sep 17 00:00:00 2001 From: Lightemerald Date: Sun, 31 Mar 2024 20:50:58 +0200 Subject: [PATCH 3/6] 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 --- .env.sample | 19 ++++++++--- .gitignore | 5 ++- README.md | 38 +++++++++++++++++++++ bun.lockb | Bin 97275 -> 105855 bytes database.sql | 2 +- index.js | 69 ++++++++++++++++++++++++++++++-------- modules/mailHandler.js | 6 ++-- modules/requestHandler.js | 20 ++++++++++- package.json | 4 ++- routes/users.js | 18 +++++----- 10 files changed, 147 insertions(+), 34 deletions(-) diff --git a/.env.sample b/.env.sample index 81e198c..2feb727 100644 --- a/.env.sample +++ b/.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 diff --git a/.gitignore b/.gitignore index 6769c0e..123b98f 100644 --- a/.gitignore +++ b/.gitignore @@ -136,4 +136,7 @@ logs/ *.log # token -tokens/ \ No newline at end of file +tokens/ + +# certs +certs/ \ No newline at end of file diff --git a/README.md b/README.md index e69de29..d8b8a90 100644 --- a/README.md +++ b/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}`. \ No newline at end of file diff --git a/bun.lockb b/bun.lockb index 0a0b0fde2b542566fe2108da16b40267400358cf..6d0949cd886fff348160654bd6970ed717f2c515 100755 GIT binary patch delta 19593 zcmezUo%R1Nwh4Ng;-%jd#h06!O0q=XuJ3a=dgMiu@kyjx9R;HMg|ZN zo)|9A>@L8#v9h13KBJ_hpqPPyAtN;>H?@R;p_YMxL6Cu=A+@5QD7Cnlp)9ee5~S&! z1Oo#%14BbeWkITLW--G`Nd^Wk28M>*%-mGnlq80Ek`VdglEjkI;?&{?5)2G{3=9od zBp4WY7#JFgQ}a?l=A3}?lX<}Eg~0}ZB&I+uu$P(K#Vp5J3+0wf-pDLpZy^UUWtJSo z)LuCT23`h+hK$7G{PH{o27P%5-&G!LYJ-hD0|O@mLqm%kgsxCvU=U_tXn3o@z#zfE z(2$;A0Cvqy1qKEY28M=x5)j`gDM8drLg@_n7!=RBCcUEKI5_Nd*PJt^~vb ziOI>S1trA{-f9r}Yz77fAqEqM1_%QYFEESH4NlH40-47Uq%rvxi(LIjEr>bKp>(k} zgnt*xSJHvFJ14a)4-^X$IuQA?q#G>NVHZ6$4%wpa2octt) z9R?5ssP|4B`w74UaV;VV9OzTvEuu zz)+N0TAZ0y$xvhu@uaf@IMy4C9U$tCYBDg$F)%c&(qv$e1{EHT3=A?13=Io3Ar6=T zH805tqCdtO68i6kR}5ph>vbMvf7&f}{mElrHcN1yVANP2d3m43m7_~^y^gsS>gCH#zP#j#Veng4ol{reti2%Y zT454X*RWl+Y4Zaf4Q7d*Aq~~*1y=lc6?E+6jnrjXdi!?hIT>kOn!JHu!J_(2`q>Vl z*Gh{|�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 delta 15621 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 n?LaQp&&y9qovt{GQ3)g^F}-*?WB&44GZ { - 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)); \ No newline at end of file +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}`); + }); +} \ No newline at end of file diff --git a/modules/mailHandler.js b/modules/mailHandler.js index 160455d..ddc1354 100644 --- a/modules/mailHandler.js +++ b/modules/mailHandler.js @@ -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 }, }); diff --git a/modules/requestHandler.js b/modules/requestHandler.js index bdf33a3..17ae10b 100644 --- a/modules/requestHandler.js +++ b/modules/requestHandler.js @@ -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, diff --git a/package.json b/package.json index e6af035..61cbaea 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/routes/users.js b/routes/users.js index c0a2dc1..40e4eab 100644 --- a/routes/users.js +++ b/routes/users.js @@ -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 { From 82486c68e735306b008ee9d233b80325546b5fca Mon Sep 17 00:00:00 2001 From: gringoelpepito Date: Sun, 31 Mar 2024 20:12:51 +0000 Subject: [PATCH 4/6] adding @me /:userId/roles --- bun.lockb | Bin 105855 -> 110155 bytes modules/permissionManager.js | 1 + package.json | 3 ++- routes/doctors.js | 2 +- routes/users.js | 3 +++ 5 files changed, 7 insertions(+), 2 deletions(-) diff --git a/bun.lockb b/bun.lockb index 6d0949cd886fff348160654bd6970ed717f2c515..b68c39576e4aa082264b69033d602db30dd86358 100755 GIT binary patch delta 21251 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 V_eMxj-~BxlNl|!0ON@1-2eap delta 19115 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�QDvtM5VTK=L4Up!y!f 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 H(UJ=Qr4=FK diff --git a/modules/permissionManager.js b/modules/permissionManager.js index 649e4fe..80e6d47 100644 --- a/modules/permissionManager.js +++ b/modules/permissionManager.js @@ -47,6 +47,7 @@ export async function verifyPermissions(userId, permissionName, permissionType) } export async function checkIfUserEmailIsVerified(userId) { + return true; try { const [user] = await pool.execute('SELECT email_verified FROM users WHERE id = ? LIMIT 1', [userId]); if (user.length === 0) return false; diff --git a/package.json b/package.json index 61cbaea..fa8e135 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "node-cron": "^3.0.3", "nodemailer": "^6.9.10", "path": "^0.12.7", - "pino": "^8.16.2" + "pino": "^8.16.2", + "pino-pretty": "^11.0.0" } } \ No newline at end of file diff --git a/routes/doctors.js b/routes/doctors.js index d2ef380..ba26843 100644 --- a/routes/doctors.js +++ b/routes/doctors.js @@ -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'); diff --git a/routes/users.js b/routes/users.js index 40e4eab..db0ffac 100644 --- a/routes/users.js +++ b/routes/users.js @@ -327,6 +327,9 @@ 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'); From 3a7e065ba0d55320931e6928f21404cb85d770a5 Mon Sep 17 00:00:00 2001 From: Lightemerald Date: Sun, 31 Mar 2024 22:25:01 +0200 Subject: [PATCH 5/6] Updating backend - Added database self check - Added more @me handlers - Fixed issues --- database.json | 980 +++++++++++++++++++++++++++++++++++ modules/databaseManager.js | 58 ++- modules/permissionManager.js | 1 - package.json | 3 +- routes/users.js | 10 +- 5 files changed, 1045 insertions(+), 7 deletions(-) create mode 100644 database.json diff --git a/database.json b/database.json new file mode 100644 index 0000000..80da010 --- /dev/null +++ b/database.json @@ -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"] + } + ] + } + ] +} \ No newline at end of file diff --git a/modules/databaseManager.js b/modules/databaseManager.js index d29e367..7ee097c 100644 --- a/modules/databaseManager.js +++ b/modules/databaseManager.js @@ -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 }; diff --git a/modules/permissionManager.js b/modules/permissionManager.js index 80e6d47..649e4fe 100644 --- a/modules/permissionManager.js +++ b/modules/permissionManager.js @@ -47,7 +47,6 @@ export async function verifyPermissions(userId, permissionName, permissionType) } export async function checkIfUserEmailIsVerified(userId) { - return true; try { const [user] = await pool.execute('SELECT email_verified FROM users WHERE id = ? LIMIT 1', [userId]); if (user.length === 0) return false; diff --git a/package.json b/package.json index fa8e135..61cbaea 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,6 @@ "node-cron": "^3.0.3", "nodemailer": "^6.9.10", "path": "^0.12.7", - "pino": "^8.16.2", - "pino-pretty": "^11.0.0" + "pino": "^8.16.2" } } \ No newline at end of file diff --git a/routes/users.js b/routes/users.js index db0ffac..6e98774 100644 --- a/routes/users.js +++ b/routes/users.js @@ -244,6 +244,7 @@ router.patch('/password/verify', antiVerificationSpam, 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'); @@ -260,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; @@ -285,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)) { @@ -313,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 ]); @@ -327,9 +331,7 @@ 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 == '@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'); @@ -345,6 +347,7 @@ router.post('/:userId/roles', verifyToken, checkBanned, checkPermissions('user', 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 ]); @@ -363,6 +366,7 @@ router.post('/:userId/roles', verifyToken, checkBanned, checkPermissions('user', 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'); From 6071d1180b4a32026f42473b894db152b354e551 Mon Sep 17 00:00:00 2001 From: gringoelpepito Date: Sun, 14 Apr 2024 19:53:16 +0000 Subject: [PATCH 6/6] Adding services routes --- index.js | 2 + modules/permissionManager.js | 1 + routes/doctors.js | 30 +++++++-- routes/hospitals.js | 2 +- routes/services.js | 122 +++++++++++++++++++++++++++++++++++ routes/users.js | 2 +- 6 files changed, 153 insertions(+), 6 deletions(-) create mode 100644 routes/services.js diff --git a/index.js b/index.js index b1ed281..b8082bd 100644 --- a/index.js +++ b/index.js @@ -16,6 +16,7 @@ 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'; @@ -50,6 +51,7 @@ 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); diff --git a/modules/permissionManager.js b/modules/permissionManager.js index 649e4fe..229c779 100644 --- a/modules/permissionManager.js +++ b/modules/permissionManager.js @@ -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; diff --git a/routes/doctors.js b/routes/doctors.js index ba26843..8532eee 100644 --- a/routes/doctors.js +++ b/routes/doctors.js @@ -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) { @@ -436,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; diff --git a/routes/hospitals.js b/routes/hospitals.js index ed72538..eb65eee 100644 --- a/routes/hospitals.js +++ b/routes/hospitals.js @@ -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], ); diff --git a/routes/services.js b/routes/services.js new file mode 100644 index 0000000..690281a --- /dev/null +++ b/routes/services.js @@ -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; \ No newline at end of file diff --git a/routes/users.js b/routes/users.js index 6e98774..8fe7952 100644 --- a/routes/users.js +++ b/routes/users.js @@ -335,7 +335,7 @@ router.get('/:userId/roles', verifyToken, checkBanned, async (req, res) => { 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, rows); + return await respondWithStatusJSON(res, 200, rows); } catch (err) { error(err);