diff --git a/apps/dashboard-api/src/__tests__/loadProjectForAdmin.test.js b/apps/dashboard-api/src/__tests__/loadProjectForAdmin.test.js index fd5d4d722..44abf3001 100644 --- a/apps/dashboard-api/src/__tests__/loadProjectForAdmin.test.js +++ b/apps/dashboard-api/src/__tests__/loadProjectForAdmin.test.js @@ -7,14 +7,24 @@ class AppError extends Error { } } +const mockAppError = AppError; + jest.mock('@urbackend/common', () => ({ - AppError, + AppError: mockAppError, Project: { findOne: jest.fn() }, getProjectAccessQuery: jest.fn((userId) => ({ $or: [{ owner: userId }, { "members.user": userId }] })) })); +jest.mock('mongoose', () => ({ + Types: { + ObjectId: { + isValid: jest.fn((value) => value === '507f1f77bcf86cd799439011') + } + } +})); + const { Project } = require('@urbackend/common'); const loadProjectForAdmin = require('../middlewares/loadProjectForAdmin'); @@ -43,13 +53,26 @@ describe('loadProjectForAdmin Middleware', () => { expect(Project.findOne).not.toHaveBeenCalled(); }); + it('should call next with AppError(400) if projectId is invalid', async () => { + req.params.projectId = 'bad-id'; + + await loadProjectForAdmin(req, res, next); + + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(expect.any(AppError)); + const error = next.mock.calls[0][0]; + expect(error.statusCode).toBe(400); + expect(error.message).toBe("Invalid project ID format"); + expect(Project.findOne).not.toHaveBeenCalled(); + }); + it('should call next with AppError(404) if project is not found', async () => { - req.params.projectId = 'proj123'; + req.params.projectId = '507f1f77bcf86cd799439011'; Project.findOne.mockResolvedValueOnce(null); await loadProjectForAdmin(req, res, next); - expect(Project.findOne).toHaveBeenCalledWith({ _id: 'proj123', $or: [{ owner: 'user123' }, { "members.user": 'user123' }] }); + expect(Project.findOne).toHaveBeenCalledWith({ _id: '507f1f77bcf86cd799439011', $or: [{ owner: 'user123' }, { "members.user": 'user123' }] }); expect(next).toHaveBeenCalledTimes(1); expect(next).toHaveBeenCalledWith(expect.any(AppError)); const error = next.mock.calls[0][0]; @@ -58,13 +81,13 @@ describe('loadProjectForAdmin Middleware', () => { }); it('should set req.project and call next without error if project is found', async () => { - req.params.projectId = 'proj123'; - const mockProject = { _id: 'proj123', name: 'Test Project' }; + req.params.projectId = '507f1f77bcf86cd799439011'; + const mockProject = { _id: '507f1f77bcf86cd799439011', name: 'Test Project' }; Project.findOne.mockResolvedValueOnce(mockProject); await loadProjectForAdmin(req, res, next); - expect(Project.findOne).toHaveBeenCalledWith({ _id: 'proj123', $or: [{ owner: 'user123' }, { "members.user": 'user123' }] }); + expect(Project.findOne).toHaveBeenCalledWith({ _id: '507f1f77bcf86cd799439011', $or: [{ owner: 'user123' }, { "members.user": 'user123' }] }); expect(req.project).toEqual(mockProject); expect(next).toHaveBeenCalledTimes(1); expect(next).toHaveBeenCalledWith(); diff --git a/apps/dashboard-api/src/controllers/project.controller.js b/apps/dashboard-api/src/controllers/project.controller.js index df7efc20a..92b5cc352 100644 --- a/apps/dashboard-api/src/controllers/project.controller.js +++ b/apps/dashboard-api/src/controllers/project.controller.js @@ -453,13 +453,17 @@ module.exports.getSingleProject = async (req, res) => { await setProjectById(req.params.projectId, projectObj); } - if (!getProjectRole(projectObj, req.user._id)) { - throw new AppError(403, "Access denied."); - } - res.json(sanitizeProjectResponse(projectObj)); } catch (err) { - res.status(500).json({ error: err.message }); + if (err instanceof AppError) { + return res.status(err.statusCode).json({ + success: false, + data: {}, + message: err.message, + }); + } + + res.status(500).json({ success: false, data: {}, message: err.message }); } }; diff --git a/apps/dashboard-api/src/middlewares/loadProjectForAdmin.js b/apps/dashboard-api/src/middlewares/loadProjectForAdmin.js index a1ad2b5c5..e219c338b 100644 --- a/apps/dashboard-api/src/middlewares/loadProjectForAdmin.js +++ b/apps/dashboard-api/src/middlewares/loadProjectForAdmin.js @@ -1,12 +1,17 @@ // FUNCTION - LOAD PROJECT FOR ADMIN (MIDDLEWARE) const { Project, AppError, getProjectAccessQuery } = require('@urbackend/common'); +const mongoose = require('mongoose'); module.exports = async (req, res, next) => { try { const { projectId } = req.params; if (!projectId) return next(new AppError(400, "Project ID is required")); + if (!mongoose.Types.ObjectId.isValid(projectId)) { + return next(new AppError(400, "Invalid project ID format")); + } + const project = await Project.findOne({ _id: projectId, ...getProjectAccessQuery(req.user._id), diff --git a/apps/web-dashboard/src/pages/Auth.jsx b/apps/web-dashboard/src/pages/Auth.jsx index 1967fb662..51cdd23db 100644 --- a/apps/web-dashboard/src/pages/Auth.jsx +++ b/apps/web-dashboard/src/pages/Auth.jsx @@ -15,6 +15,22 @@ import AddRecordDrawer from '../components/AddRecordDrawer'; import { PUBLIC_API_URL } from '../config'; export default function Auth() { + const unwrapApiResponse = (payload) => { + if (!payload || typeof payload !== 'object') { + return { ok: true, data: payload, message: '' }; + } + + if (Object.prototype.hasOwnProperty.call(payload, 'success')) { + return { + ok: payload.success !== false, + data: payload.data ?? payload.project ?? payload, + message: payload.message || payload.error || '' + }; + } + + return { ok: true, data: payload, message: '' }; + }; + const normalizeUsersResponse = (payload) => { if (Array.isArray(payload)) return payload; if (Array.isArray(payload?.items)) return payload.items; @@ -87,25 +103,39 @@ export default function Auth() { const fetchData = async () => { try { const projRes = await api.get(`/api/projects/${projectId}`); + const projectResult = unwrapApiResponse(projRes.data); + + if (!projectResult.ok) { + throw new Error(projectResult.message || 'Failed to load auth details'); + } + if (isMounted) { - setProject(projRes.data); - if (projRes.data.authProviders) setAuthProviders(projRes.data.authProviders); - if (projRes.data.isAuthEnabled) { + setProject(projectResult.data); + if (projectResult.data?.authProviders) setAuthProviders(projectResult.data.authProviders); + if (projectResult.data?.isAuthEnabled) { const requestId = ++latestUsersRequestId.current; const usersRes = await api.get( `/api/projects/${projectId}/admin/users?page=${page}&limit=${limit}` ); + const usersResult = unwrapApiResponse(usersRes.data); + + if (!usersResult.ok) { + throw new Error(usersResult.message || 'Failed to load auth users'); + } + if (!isMounted || requestId !== latestUsersRequestId.current) return; - setUsers(normalizeUsersResponse(usersRes.data)); + setUsers(normalizeUsersResponse(usersResult.data)); setTotalRecords( - usersRes.data?.data?.total || - usersRes.data?.total || - normalizeUsersResponse(usersRes.data).length + usersResult.data?.data?.total || + usersResult.data?.total || + normalizeUsersResponse(usersResult.data).length ); } } - } catch { toast.error("Failed to load auth details"); } + } catch (err) { + toast.error(err?.response?.data?.message || err?.response?.data?.error || err.message || "Failed to load auth details"); + } finally { if (isMounted) setLoading(false); } }; fetchData(); diff --git a/packages/common/src/middleware/loadProjectForAdmin.js b/packages/common/src/middleware/loadProjectForAdmin.js index a9d30a214..de61349a9 100644 --- a/packages/common/src/middleware/loadProjectForAdmin.js +++ b/packages/common/src/middleware/loadProjectForAdmin.js @@ -1,13 +1,21 @@ // FUNCTION - LOAD PROJECT FOR ADMIN (MIDDLEWARE) const Project = require('../models/Project'); const AppError = require('../utils/AppError'); +const { getProjectAccessQuery } = require('../utils/projectAccess'); module.exports = async (req, res, next) => { try { const { projectId } = req.params; if (!projectId) return next(new AppError(400, "Project ID is required")); - const project = await Project.findOne({ _id: projectId, owner: req.user._id }); + if (!mongoose.Types.ObjectId.isValid(projectId)) { + return next(new AppError(400, "Invalid project ID format")); + } + + const project = await Project.findOne({ + _id: projectId, + ...getProjectAccessQuery(req.user._id), + }); if (!project) { return next(new AppError(404, "Project not found or access denied")); }