diff --git a/client/package.json b/client/package.json index 128c729..f8c2ec7 100644 --- a/client/package.json +++ b/client/package.json @@ -5,6 +5,7 @@ "type": "module", "scripts": { "dev": "vite", + "start": "vite", "build": "vite build", "lint": "eslint .", "preview": "vite preview", diff --git a/client/src/pages/Login.jsx b/client/src/pages/Login.jsx index 79fede1..bef3a57 100644 --- a/client/src/pages/Login.jsx +++ b/client/src/pages/Login.jsx @@ -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; diff --git a/client/src/utils/errorMessage.js b/client/src/utils/errorMessage.js index c596f0d..f6bdd6c 100644 --- a/client/src/utils/errorMessage.js +++ b/client/src/utils/errorMessage.js @@ -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 } diff --git a/server/src/config/socket.js b/server/src/config/socket.js index a0ff158..1fef49d 100644 --- a/server/src/config/socket.js +++ b/server/src/config/socket.js @@ -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')); diff --git a/server/src/routes/authRoutes.js b/server/src/routes/authRoutes.js index 689f0f9..34cf991 100644 --- a/server/src/routes/authRoutes.js +++ b/server/src/routes/authRoutes.js @@ -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 = [ diff --git a/server/src/server.js b/server/src/server.js index 460c050..186bd9e 100644 --- a/server/src/server.js +++ b/server/src/server.js @@ -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'], }) );