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
1 change: 1 addition & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"type": "module",
"scripts": {
"dev": "vite",
"start": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview",
Expand Down
11 changes: 10 additions & 1 deletion client/src/pages/Login.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,19 @@ const Login = () => {

const handleSubmit = async (e) => {
e.preventDefault();
const cleanId = (identifier || '').trim();
if (!cleanId) {
setError('Please enter your Email, MAVI ID, or PRN.');
return;
}
if (!password) {
setError('Please enter your password.');
return;
}
setError('');
setSubmitting(true);
try {
const data = await login(identifier, password);
const data = await login(cleanId, password);
toast.success('Successfully authenticated!');

const role = data?.user?.role;
Expand Down
9 changes: 6 additions & 3 deletions client/src/utils/errorMessage.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@ export function getErrorMessage(error, fallback = 'Something went wrong. Please

const { status, data } = error.response;

// express-validator style: { errors: [{ message }] }
if (data?.errors && Array.isArray(data.errors) && data.errors.length > 0 && data.errors[0]?.message) {
return data.errors[0].message;
// express-validator style: { errors: [{ message || msg }] }
if (data?.errors && Array.isArray(data.errors) && data.errors.length > 0) {
const firstErr = data.errors[0];
if (firstErr?.message) return firstErr.message;
if (firstErr?.msg) return firstErr.msg;
if (typeof firstErr === 'string') return firstErr;
}

// Standard API shape used across this app: { success: false, message }
Expand Down
28 changes: 21 additions & 7 deletions server/src/config/socket.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,32 @@ let io;

module.exports = {
init: (httpServer) => {
const allowedOrigins = [
const defaultAllowedOrigins = [
'http://localhost:5173',
process.env.CLIENT_URL,
].filter(Boolean);
'http://127.0.0.1:5173',
'http://localhost:3000',
'https://mavi-linking-mq7d.vercel.app',
];

const envAllowedOrigins = (process.env.CLIENT_URL || '')
.split(',')
.map((o) => o.trim())
.filter(Boolean);

const allowedOrigins = [...new Set([...defaultAllowedOrigins, ...envAllowedOrigins])];

const isOriginAllowed = (origin) => {
if (!origin) return true;
const cleanOrigin = origin.replace(/\/+$/, '');
if (allowedOrigins.some((o) => o.replace(/\/+$/, '') === cleanOrigin)) return true;
if (/\.vercel\.app$/i.test(cleanOrigin) || /\.onrender\.com$/i.test(cleanOrigin)) return true;
return false;
};

io = new Server(httpServer, {
cors: {
origin: (origin, callback) => {
if (!origin) return callback(null, true);
const cleanOrigins = allowedOrigins.map(o => o.replace(/\/+$/, ''));
const cleanOrigin = origin.replace(/\/+$/, '');
if (cleanOrigins.includes(cleanOrigin)) {
if (isOriginAllowed(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
Expand Down
8 changes: 8 additions & 0 deletions server/src/routes/authRoutes.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,14 @@ const loginValidation = [
body('password')
.notEmpty()
.withMessage('Password is required'),
body()
.custom((_, { req }) => {
const id = req.body.identifier || req.body.email || req.body.maviId || req.body.prn;
if (!id || !id.toString().trim()) {
throw new Error('Email, MAVI ID, or PRN is required');
}
return true;
}),
];

const updateProfileValidation = [
Expand Down
35 changes: 22 additions & 13 deletions server/src/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,31 +76,40 @@ app.use(helmet({
},
})); // Security headers

const allowedOrigins = [
const defaultAllowedOrigins = [
'http://localhost:5173',
'http://127.0.0.1:5173',
'http://localhost:3000',
'https://mavi-linking-mq7d.vercel.app',
process.env.CLIENT_URL,
].filter(Boolean);
];

const envAllowedOrigins = (process.env.CLIENT_URL || '')
.split(',')
.map((o) => o.trim())
.filter(Boolean);

const allowedOrigins = [...new Set([...defaultAllowedOrigins, ...envAllowedOrigins])];

const isOriginAllowed = (origin) => {
if (!origin) return true; // Allow direct server-to-server, health checks, postman, webhooks
const cleanOrigin = origin.replace(/\/+$/, '');
if (allowedOrigins.some((o) => o.replace(/\/+$/, '') === cleanOrigin)) return true;
// Allow all vercel.app and onrender.com preview/production deployments
if (/\.vercel\.app$/i.test(cleanOrigin) || /\.onrender\.com$/i.test(cleanOrigin)) return true;
return false;
};

app.use(
cors({
origin: function (origin, callback) {
// Allow requests with no origin (mobile apps, Postman, etc.) only in development
if (!origin) {
if (process.env.NODE_ENV === 'production') {
return callback(new Error('Not allowed by CORS'));
}
return callback(null, true);
}
if (allowedOrigins.includes(origin)) {
if (isOriginAllowed(origin)) {
return callback(null, true);
}
return callback(new Error('Not allowed by CORS'));
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
allowedHeaders: ['Content-Type', 'Authorization'],
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
})
);

Expand Down
Loading