Skip to content

Commit a02d428

Browse files
committed
feat: implement doctor working hours system with RBAC and secure middleware
1 parent 1d707b4 commit a02d428

8 files changed

Lines changed: 211 additions & 14 deletions

File tree

src/controllers/admincontroller.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,23 @@ export const login = async (req, res) => {
3939
error: error.message,
4040
});
4141
}
42+
};
43+
44+
export const updateDoctorWorkingHours = async (req, res) => {
45+
try {
46+
47+
if (req.user.role !== 'admin') {
48+
return res.status(403).json({ message: "Access denied" });
49+
}
50+
51+
const result = await doctorService.updateWorkingHours(
52+
req.params.id,
53+
req.body.workingHours
54+
);
55+
56+
res.status(200).json(result);
57+
58+
} catch (error) {
59+
res.status(400).json({ message: error.message });
60+
}
4261
};

src/controllers/doctor.controller.js

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,31 @@ const deleteDoctor = async (req, res) => {
5050
res.status(500).json({ message: err.message });
5151
}
5252
};
53+
54+
const updateMyWorkingHours = async (req, res) => {
55+
try {
56+
57+
if (req.user.role !== 'doctor') {
58+
return res.status(403).json({ message: "Access denied" });
59+
}
60+
61+
const result = await doctorService.updateWorkingHours(
62+
req.user.id,
63+
req.body.workingHours
64+
);
65+
66+
res.status(200).json(result);
67+
68+
} catch (error) {
69+
res.status(400).json({ message: error.message });
70+
}
71+
}
5372

5473
export {
5574
createDoctor,
5675
changePassword,
5776
getDoctors,
5877
updateDoctor,
59-
deleteDoctor
78+
deleteDoctor,
79+
updateMyWorkingHours
6080
};

src/middleware/authmiddleware.js

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import jwt from 'jsonwebtoken';
2+
import User from '../models/User.js';
23

3-
export const protect = (req, res, next) => {
4+
export const protect = async (req, res, next) => {
45
let token;
56

67
if (
@@ -12,13 +13,30 @@ export const protect = (req, res, next) => {
1213

1314
const decoded = jwt.verify(token, process.env.JWT_SECRET);
1415

16+
const user = await User.findById(decoded.id).select('-password');
17+
18+
if (!user) {
19+
return res.status(401).json({
20+
success: false,
21+
message: "User no longer exists",
22+
});
23+
}
24+
25+
if (user.status === 'Inactive') {
26+
return res.status(401).json({
27+
success: false,
28+
message: "User account is inactive",
29+
});
30+
}
31+
1532
req.user = {
16-
id: decoded.id,
17-
role: decoded.role.toLowerCase(),
18-
email: decoded.email,
33+
id: user._id,
34+
role: user.role.toLowerCase(),
35+
email: user.email,
1936
};
2037

2138
return next();
39+
2240
} catch (error) {
2341
return res.status(401).json({
2442
success: false,

src/models/User.js

Lines changed: 107 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,143 @@
11
import mongoose from 'mongoose';
22

3+
const timeRegex = /^([01]\d|2[0-3]):([0-5]\d)$/;
4+
5+
const slotSchema = new mongoose.Schema({
6+
startTime: {
7+
type: String,
8+
required: true,
9+
match: timeRegex
10+
},
11+
endTime: {
12+
type: String,
13+
required: true,
14+
match: timeRegex
15+
}
16+
}, { _id: false });
17+
18+
const workingDaySchema = new mongoose.Schema({
19+
day: {
20+
type: String,
21+
required: true,
22+
enum: ["Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"]
23+
},
24+
isAvailable: {
25+
type: Boolean,
26+
default: true
27+
},
28+
slots: {
29+
type: [slotSchema],
30+
default: []
31+
}
32+
}, { _id: false });
33+
34+
workingDaySchema.pre('validate', function(next) {
35+
36+
if (!this.isAvailable && this.slots.length > 0) {
37+
return next(new Error("Slots cannot exist when doctor is unavailable"));
38+
}
39+
40+
const sorted = [...this.slots].sort((a, b) =>
41+
a.startTime.localeCompare(b.startTime)
42+
);
43+
44+
for (let i = 0; i < sorted.length; i++) {
45+
46+
if (sorted[i].startTime >= sorted[i].endTime) {
47+
return next(new Error("Start time must be before end time"));
48+
}
49+
50+
if (i > 0 && sorted[i - 1].endTime > sorted[i].startTime) {
51+
return next(new Error("Overlapping time slots detected"));
52+
}
53+
}
54+
55+
next();
56+
});
57+
58+
const DEFAULT_WORKING_HOURS = [
59+
{ day: "Monday", isAvailable: true, slots: [{ startTime: "09:00", endTime: "17:00" }] },
60+
{ day: "Tuesday", isAvailable: true, slots: [{ startTime: "09:00", endTime: "17:00" }] },
61+
{ day: "Wednesday", isAvailable: true, slots: [{ startTime: "09:00", endTime: "17:00" }] },
62+
{ day: "Thursday", isAvailable: true, slots: [{ startTime: "09:00", endTime: "17:00" }] },
63+
{ day: "Friday", isAvailable: true, slots: [{ startTime: "09:00", endTime: "17:00" }] },
64+
{ day: "Saturday", isAvailable: false, slots: [] },
65+
{ day: "Sunday", isAvailable: false, slots: [] },
66+
];
67+
368
const userSchema = new mongoose.Schema({
69+
470
email: {
571
type: String,
672
required: true,
773
unique: true,
8-
match: /.+\@.+\..+/
74+
match: /.+\@.+\..+/
75+
},
76+
77+
password: {
78+
type: String,
79+
required: true
980
},
10-
password: { type: String, required: true },
1181

1282
role: {
1383
type: String,
1484
required: true,
1585
enum: ['admin', 'doctor','receptionist','billing']
1686
},
1787

18-
1988
name: { type: String },
89+
2090
phno: {
2191
type: String,
2292
match: /^[0-9]{10}$/
2393
},
94+
2495
spec: { type: String },
96+
2597
dept: {
2698
type: mongoose.Schema.Types.ObjectId,
2799
ref: 'Department'
28100
},
101+
29102
exp: { type: String },
30103
qual: { type: String },
104+
31105
status: {
32106
type: String,
33107
enum: ['Active', 'Inactive'],
34108
default: 'Active'
109+
},
110+
111+
workingHours: {
112+
type: [workingDaySchema]
35113
}
114+
36115
}, { timestamps: true });
37116

117+
118+
userSchema.pre('validate', function(next) {
119+
120+
if (this.role === 'doctor' && this.workingHours) {
121+
const days = this.workingHours.map(d => d.day);
122+
const uniqueDays = new Set(days);
123+
124+
if (days.length !== uniqueDays.size) {
125+
return next(new Error("Duplicate days are not allowed"));
126+
}
127+
}
128+
129+
next();
130+
});
131+
132+
userSchema.pre('save', function(next) {
133+
134+
if (this.role === 'doctor' && (!this.workingHours || this.workingHours.length === 0)) {
135+
this.workingHours = DEFAULT_WORKING_HOURS;
136+
}
137+
138+
next();
139+
});
140+
141+
38142
const User = mongoose.models.User || mongoose.model('User', userSchema);
39143
export default User;

src/routes/adminroutes.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
import express from 'express';
22
import * as adminController from '../controllers/admincontroller.js';
3-
3+
import { protect, authorize } from '../middleware/authmiddleware.js';
44
const router = express.Router();
55

66
router.post('/login', adminController.login);
77

8-
export default router;
8+
router.put(
9+
'/doctors/:id/working-hours',
10+
protect,
11+
authorize(['admin']),
12+
adminController.updateDoctorWorkingHours
13+
);
14+
15+
export default router;

src/routes/doctor.routes.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ router.put('/change-password', protect, authorize('doctor'), doctorController.ch
1212

1313
router.put('/:id', protect, authorize('admin'), doctorController.updateDoctor);
1414

15+
router.put('/me/working-hours',
16+
protect,
17+
authorize('doctor'),
18+
doctorController.updateMyWorkingHours
19+
);
20+
1521
router.delete('/:id', protect, authorize('admin'), doctorController.deleteDoctor);
1622

17-
export default router;
23+
export default router;

src/services/billing.service.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ const updateBillingStaff = async (id, data) => {
4646
const billing = await User.findOneAndUpdate(
4747
{ _id: id, role: 'billing' },
4848
data,
49-
{ new: true, runvalidators: true }
49+
{ new: true, runValidators: true }
5050
);
5151

5252
if(!billing) throw new Error('Billing staff not found');

src/services/doctor.service.js

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,18 @@ const getDoctors = async () => {
3838
};
3939

4040
const updateDoctor = async (id, data) => {
41+
42+
if(data.workingHours){
43+
throw new Error('User dedicated endpoint to update working hours');
44+
}
4145
if(data.password) {
4246
data.password = await bcrypt.hash(data.password, 10);
4347
}
4448

4549
const doctor = await User.findOneAndUpdate(
4650
{ _id: id, role: 'doctor' },
4751
data,
48-
{ new: true, runvalidators: true }
52+
{ new: true, runValidators: true }
4953
);
5054

5155
if(!doctor) throw new Error('Doctor not found');
@@ -60,10 +64,29 @@ const deleteDoctor = async (id) => {
6064
return { message: 'Doctor deleted successfully' };
6165
};
6266

67+
const updateWorkingHours = async (doctorId, workingHours) => {
68+
69+
const doctor = await User.findOne({
70+
_id: doctorId,
71+
role: 'doctor'
72+
});
73+
74+
if (!doctor) throw new Error("Doctor not found");
75+
76+
doctor.workingHours = workingHours;
77+
78+
await doctor.save();
79+
80+
return {
81+
message: "Working hours updated successfully"
82+
};
83+
};
84+
6385
export default{
6486
createDoctor,
6587
changePassword,
6688
getDoctors,
6789
updateDoctor,
68-
deleteDoctor
90+
deleteDoctor,
91+
updateWorkingHours
6992
};

0 commit comments

Comments
 (0)