Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
347 changes: 347 additions & 0 deletions backend/package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"cors": "^2.8.6",
"dotenv": "^17.4.2",
"express": "^5.2.1",
"express-rate-limit": "^8.5.2",
"express-validator": "^7.3.2",
"helmet": "^8.1.0",
"jsonwebtoken": "^9.0.3",
Expand All @@ -27,6 +28,7 @@
"multer": "^2.1.1",
"pdf-parse": "^2.4.5",
"sharp": "^0.34.5",
"socket.io": "^4.8.3",
"tesseract.js": "^7.0.0"
},
"devDependencies": {
Expand Down
50 changes: 38 additions & 12 deletions backend/server.js
Original file line number Diff line number Diff line change
@@ -1,34 +1,41 @@
import express from 'express';
import http from 'http';
import mongoose from 'mongoose';
import cors from 'cors';
import helmet from 'helmet';
import morgan from 'morgan';
import compression from 'compression';
import dotenv from 'dotenv';

import { initSocket } from './src/socket/socket.js';
import { apiLimiter } from './src/middleware/rateLimit.middleware.js';
import { logAudit } from './src/middleware/audit.middleware.js';

dotenv.config();

const app = express();
const server = http.createServer(app);

// Security middleware
app.use(helmet());
// Security & utility middleware
app.use(helmet({
crossOriginResourcePolicy: false // Allow static /uploads images/files
}));
app.use(cors({
origin: 'http://localhost:5173',
origin: ['http://localhost:5173', 'http://127.0.0.1:5173', '*'],
credentials: true
}));
Comment on lines 22 to 25

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

Critical CORS misconfiguration: credentialed requests allowed from any origin.

With credentials: true and '*' in the origin array, the cors package reflects any request origin back in Access-Control-Allow-Origin while also sending Access-Control-Allow-Credentials: true. This allows any website to make credentialed cross-origin requests to the API, enabling CSRF-style attacks if cookies or other browser-attached credentials are ever used. The '*' entry also renders the specific localhost entries pointless.

Remove '*' and restrict origins to trusted origins only. For production, drive this via an environment variable.

🔒 Proposed fix for CORS configuration
 app.use(cors({
-  origin: ['http://localhost:5173', 'http://127.0.0.1:5173', '*'],
+  origin: (process.env.CORS_ORIGINS || 'http://localhost:5173,http://127.0.0.1:5173').split(','),
   credentials: true
 }));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
app.use(cors({
origin: 'http://localhost:5173',
origin: ['http://localhost:5173', 'http://127.0.0.1:5173', '*'],
credentials: true
}));
app.use(cors({
origin: (process.env.CORS_ORIGINS || 'http://localhost:5173,http://127.0.0.1:5173').split(','),
credentials: true
}));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/server.js` around lines 22 - 25, Restrict the CORS allowlist in the
`app.use(cors(...))` configuration by removing `'*'` and retaining only trusted
origins. Make the production origins configurable through an environment
variable while preserving `credentials: true` for approved origins.

app.use(compression());
app.use(morgan('combined'));
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
app.use(express.json({ limit: '50mb' }));
app.use(express.urlencoded({ extended: true, limit: '50mb' }));

// Rate limiter for API routes
app.use('/api/', apiLimiter);

// Static files for uploads
app.use('/uploads', express.static('uploads'));

// Database connection
mongoose.connect(process.env.MONGODB_URI, {

})
mongoose.connect(process.env.MONGODB_URI, {})
.then(() => console.log('✅ MongoDB Connected'))
.catch(err => console.error('❌ MongoDB connection error:', err));

Expand All @@ -37,12 +44,28 @@ import authRoutes from './src/routes/auth.routes.js';
import prescriptionRoutes from './src/routes/prescription.routes.js';
import symptomRoutes from './src/routes/symptom.routes.js';
import reportRoutes from './src/routes/report.routes.js';
import doctorRoutes from './src/routes/doctor.routes.js';
import patientRoutes from './src/routes/patient.routes.js';
import appointmentRoutes from './src/routes/appointment.routes.js';
import chatRoutes from './src/routes/chat.routes.js';
import digitalPrescriptionRoutes from './src/routes/digitalPrescription.routes.js';
import paymentRoutes from './src/routes/payment.routes.js';
import notificationRoutes from './src/routes/notification.routes.js';
import adminRoutes from './src/routes/admin.routes.js';

// Use routes
app.use('/api/auth', authRoutes);
app.use('/api/prescriptions', prescriptionRoutes);
app.use('/api/symptoms', symptomRoutes);
app.use('/api/reports', reportRoutes);
app.use('/api/doctors', doctorRoutes);
app.use('/api/patients', patientRoutes);
app.use('/api/appointments', appointmentRoutes);
app.use('/api/chat', chatRoutes);
app.use('/api/digital-prescriptions', digitalPrescriptionRoutes);
app.use('/api/payments', paymentRoutes);
app.use('/api/notifications', notificationRoutes);
app.use('/api/admin', adminRoutes);

// Error handling middleware
app.use((err, req, res, next) => {
Expand All @@ -53,7 +76,10 @@ app.use((err, req, res, next) => {
});
});

const PORT = process.env.PORT || 5000;
app.listen(PORT, () => {
console.log(`🚀 Server running on port ${PORT}`);
// Initialize Socket.IO
initSocket(server);

const PORT = process.env.PORT || 4500;
server.listen(PORT, () => {
console.log(`🚀 Server and Socket.IO running on port ${PORT}`);
});
172 changes: 172 additions & 0 deletions backend/src/controllers/admin.controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import User from '../models/User.model.js';
import DoctorProfile from '../models/DoctorProfile.model.js';
import PatientProfile from '../models/PatientProfile.model.js';
import Appointment from '../models/Appointment.model.js';
import Payment from '../models/Payment.model.js';
import AuditLog from '../models/AuditLog.model.js';
import Notification from '../models/Notification.model.js';
import { createAuditLog } from '../middleware/audit.middleware.js';

// ---------------- GET PLATFORM ANALYTICS ----------------
export const getPlatformAnalytics = async (req, res) => {
try {
const [
totalPatients,
totalDoctors,
totalAppointments,
pendingVerifications,
completedPayments,
recentLogs
] = await Promise.all([
User.countDocuments({ role: 'patient' }),
User.countDocuments({ role: 'doctor' }),
Appointment.countDocuments(),
DoctorProfile.countDocuments({ verificationStatus: 'pending' }),
Payment.find({ status: 'completed' }),
AuditLog.find().populate('userId', 'name email role').sort('-createdAt').limit(15)
]);

const totalRevenue = completedPayments.reduce((acc, curr) => acc + (curr.amount || 0), 0);

res.json({
success: true,
analytics: {
totalPatients,
totalDoctors,
totalAppointments,
pendingVerifications,
totalRevenue,
recentLogs
}
});
} catch (error) {
console.error('Get Platform Analytics Error:', error);
res.status(500).json({ success: false, message: error.message });
}
};

// ---------------- VERIFY OR REJECT DOCTOR ----------------
export const verifyDoctor = async (req, res) => {
try {
const { profileId } = req.params;
const { status, rejectionReason } = req.body; // 'verified' or 'rejected'

if (!['verified', 'rejected', 'pending'].includes(status)) {
return res.status(400).json({ success: false, message: 'Invalid status. Use verified, rejected, or pending' });
}

const doctorProfile = await DoctorProfile.findById(profileId).populate('userId', 'name email status isVerified');
if (!doctorProfile) {
return res.status(404).json({ success: false, message: 'Doctor profile not found' });
}

doctorProfile.verificationStatus = status;
if (status === 'rejected') {
doctorProfile.rejectionReason = rejectionReason || 'License verification failed';
} else {
doctorProfile.rejectionReason = '';
if (doctorProfile.userId) {
doctorProfile.userId.isVerified = true;
await doctorProfile.userId.save();
}
}
Comment on lines +63 to +72

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

verifyDoctor incorrectly sets isVerified = true for pending status.

The else branch covers both verified and pending, so setting status to pending still calls doctorProfile.userId.isVerified = true. Only verified should trigger this.

🐛 Proposed fix
     doctorProfile.verificationStatus = status;
     if (status === 'rejected') {
       doctorProfile.rejectionReason = rejectionReason || 'License verification failed';
-    } else {
+    } else if (status === 'verified') {
       doctorProfile.rejectionReason = '';
       if (doctorProfile.userId) {
         doctorProfile.userId.isVerified = true;
         await doctorProfile.userId.save();
       }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
doctorProfile.verificationStatus = status;
if (status === 'rejected') {
doctorProfile.rejectionReason = rejectionReason || 'License verification failed';
} else {
doctorProfile.rejectionReason = '';
if (doctorProfile.userId) {
doctorProfile.userId.isVerified = true;
await doctorProfile.userId.save();
}
}
doctorProfile.verificationStatus = status;
if (status === 'rejected') {
doctorProfile.rejectionReason = rejectionReason || 'License verification failed';
} else if (status === 'verified') {
doctorProfile.rejectionReason = '';
if (doctorProfile.userId) {
doctorProfile.userId.isVerified = true;
await doctorProfile.userId.save();
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/controllers/admin.controller.js` around lines 63 - 72, Update
verifyDoctor so user verification is enabled only when status === 'verified';
keep rejectionReason cleared for other non-rejected statuses, but move the
doctorProfile.userId.isVerified assignment and save into a dedicated
verified-status branch.

await doctorProfile.save();

// Send notification
if (doctorProfile.userId) {
await Notification.create({
userId: doctorProfile.userId._id,
title: `Doctor Verification ${status.toUpperCase()}`,
message: status === 'verified'
? 'Congratulations! Your medical license has been verified by the administrator.'
: `Your doctor verification was rejected: ${rejectionReason || 'Incomplete documentation'}`,
type: 'system',
link: '/doctor-dashboard',
relatedId: doctorProfile._id
});
}

await createAuditLog(req.user._id, `Verified doctor profile ${doctorProfile.userId?.name} status -> ${status}`);

res.json({ success: true, message: `Doctor profile status updated to ${status}`, doctorProfile });
} catch (error) {
console.error('Verify Doctor Error:', error);
res.status(500).json({ success: false, message: error.message });
}
};

// ---------------- GET ALL USERS ----------------
export const getAllUsers = async (req, res) => {
try {
const { role, status, search } = req.query;
const filter = {};
if (role && role !== 'All') filter.role = role;
if (status && status !== 'All') filter.status = status;
if (search) {
filter.$or = [
{ name: { $regex: search, $options: 'i' } },
{ email: { $regex: search, $options: 'i' } }
];
}

const users = await User.find(filter).select('-password').sort('-createdAt');
res.json({ success: true, users });
} catch (error) {
console.error('Get All Users Error:', error);
res.status(500).json({ success: false, message: error.message });
}
};

// ---------------- UPDATE USER STATUS OR ROLE ----------------
export const updateUserStatus = async (req, res) => {
try {
const { userId } = req.params;
const { status, role } = req.body;

const user = await User.findById(userId).select('-password');
if (!user) {
return res.status(404).json({ success: false, message: 'User not found' });
}

if (status) user.status = status;
if (role) {
user.role = role;
// Ensure profile exists for the new role
if (role === 'doctor') {
const docExists = await DoctorProfile.findOne({ userId });
if (!docExists) await DoctorProfile.create({ userId });
} else if (role === 'patient') {
const patExists = await PatientProfile.findOne({ userId });
if (!patExists) await PatientProfile.create({ userId });
}
}
await user.save();

await createAuditLog(req.user._id, `Updated user ${user.email} -> status: ${user.status}, role: ${user.role}`);

res.json({ success: true, message: 'User updated successfully', user });
} catch (error) {
console.error('Update User Status Error:', error);
res.status(500).json({ success: false, message: error.message });
}
};

// ---------------- GET AUDIT LOGS ----------------
export const getAuditLogs = async (req, res) => {
try {
const { action, status, limit } = req.query;
const filter = {};
if (action) filter.action = { $regex: action, $options: 'i' };
if (status && status !== 'All') filter.status = status;

const logs = await AuditLog.find(filter)
.populate('userId', 'name email role')
.sort('-createdAt')
.limit(Number(limit) || 100);

res.json({ success: true, logs });
} catch (error) {
console.error('Get Audit Logs Error:', error);
res.status(500).json({ success: false, message: error.message });
}
};
Loading