From cbec27d281e603ac19e77c227915a456f483d0ab Mon Sep 17 00:00:00 2001 From: udaycodespace Date: Thu, 11 Jun 2026 11:12:08 +0530 Subject: [PATCH 1/7] feat(auth): add pagination support to user list --- apps/web-dashboard/src/pages/Auth.jsx | 36 ++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/apps/web-dashboard/src/pages/Auth.jsx b/apps/web-dashboard/src/pages/Auth.jsx index 071c2309..ac4f4c76 100644 --- a/apps/web-dashboard/src/pages/Auth.jsx +++ b/apps/web-dashboard/src/pages/Auth.jsx @@ -8,6 +8,7 @@ import AuthHeader from '../components/Auth/AuthHeader'; import SocialAuthConfig from '../components/Auth/SocialAuthConfig'; import SocialAuthModal from '../components/Auth/SocialAuthModal'; import UserTable from '../components/Auth/UserTable'; +import Pagination from '../components/Database/Pagination'; import SectionHeader from '../components/Dashboard/SectionHeader'; import AddRecordDrawer from '../components/AddRecordDrawer'; import { PUBLIC_API_URL } from '../config'; @@ -24,6 +25,9 @@ export default function Auth() { const navigate = useNavigate(); const [users, setUsers] = useState([]); + const [page, setPage] = useState(1); + const [limit, setLimit] = useState(50); + const [totalRecords, setTotalRecords] = useState(0); const [loading, setLoading] = useState(true); const [searchTerm, setSearchTerm] = useState(''); const [project, setProject] = useState(null); @@ -76,8 +80,16 @@ export default function Auth() { setProject(projRes.data); if (projRes.data.authProviders) setAuthProviders(projRes.data.authProviders); if (projRes.data.isAuthEnabled) { - const usersRes = await api.get(`/api/projects/${projectId}/collections/users/data`); + const usersRes = await api.get( + `/api/projects/${projectId}/collections/users/data?page=${page}&limit=${limit}` + ); + setUsers(normalizeUsersResponse(usersRes.data)); + setTotalRecords( + usersRes.data?.data?.total || + usersRes.data?.total || + normalizeUsersResponse(usersRes.data).length + ); } } } catch { toast.error("Failed to load auth details"); } @@ -85,7 +97,7 @@ export default function Auth() { }; fetchData(); return () => { isMounted = false; }; - }, [projectId]); + }, [projectId, page, limit]); const handleEnableAuth = async () => { if (!hasUserCollection) return toast.error("Please create a 'users' collection first."); @@ -222,8 +234,16 @@ export default function Auth() { } else { await api.post(`/api/projects/${projectId}/admin/users`, userData); toast.success('User created successfully'); - const usersRes = await api.get(`/api/projects/${projectId}/collections/users/data`); + const usersRes = await api.get( + `/api/projects/${projectId}/collections/users/data?page=${page}&limit=${limit}` + ); + setUsers(normalizeUsersResponse(usersRes.data)); + setTotalRecords( + usersRes.data?.data?.total || + usersRes.data?.total || + normalizeUsersResponse(usersRes.data).length + ); } setIsAddModalOpen(false); setEditingUser(null); @@ -295,6 +315,16 @@ export default function Auth() { onResetPassword={(u) => { setResetPasswordUser(u); setNewPassword(''); }} onDelete={handleDeleteUser} /> + setPage(p)} + onLimitChange={(newLimit) => { + setLimit(newLimit); + setPage(1); + }} + /> From 17537044983343e9caa923ccaa5fcb0f468838ba Mon Sep 17 00:00:00 2001 From: udaycodespace Date: Thu, 11 Jun 2026 19:53:37 +0530 Subject: [PATCH 2/7] fix(auth): prevent stale pagination updates and sync totals --- apps/web-dashboard/src/pages/Auth.jsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/apps/web-dashboard/src/pages/Auth.jsx b/apps/web-dashboard/src/pages/Auth.jsx index ac4f4c76..77b7d101 100644 --- a/apps/web-dashboard/src/pages/Auth.jsx +++ b/apps/web-dashboard/src/pages/Auth.jsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useRef } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import api from '../utils/api'; import toast from 'react-hot-toast'; @@ -36,6 +36,7 @@ export default function Auth() { const [isSocialAuthModalOpen, setIsSocialAuthModalOpen] = useState(false); const [isAddModalOpen, setIsAddModalOpen] = useState(false); const [editingUser, setEditingUser] = useState(null); // user being edited + const latestUsersRequestId = useRef(0); const [selectedProvider, setSelectedProvider] = useState('github'); const [authProviders, setAuthProviders] = useState({ github: { enabled: false, clientId: '', clientSecret: '', hasClientSecret: false }, @@ -80,10 +81,12 @@ export default function Auth() { setProject(projRes.data); if (projRes.data.authProviders) setAuthProviders(projRes.data.authProviders); if (projRes.data.isAuthEnabled) { + const requestId = ++latestUsersRequestId.current; const usersRes = await api.get( `/api/projects/${projectId}/collections/users/data?page=${page}&limit=${limit}` ); + if (!isMounted || requestId !== latestUsersRequestId.current) return; setUsers(normalizeUsersResponse(usersRes.data)); setTotalRecords( usersRes.data?.data?.total || @@ -167,7 +170,10 @@ export default function Auth() { if (!confirm('Delete this user? This cannot be undone.')) return; try { await api.delete(`/api/projects/${projectId}/collections/users/data/${userId}`); - setUsers(prev => normalizeUsersResponse(prev).filter(u => u._id !== userId)); + const nextUsers = normalizeUsersResponse(users).filter(u => u._id !== userId); + setUsers(nextUsers); + setTotalRecords(prev => Math.max(prev - 1, 0)); + if (nextUsers.length === 0 && page > 1) setPage(page - 1); toast.success('User deleted'); } catch (err) { toast.error(err.response?.data?.message || err.response?.data?.error || 'Failed to delete user'); @@ -234,10 +240,12 @@ export default function Auth() { } else { await api.post(`/api/projects/${projectId}/admin/users`, userData); toast.success('User created successfully'); + const requestId = ++latestUsersRequestId.current; const usersRes = await api.get( `/api/projects/${projectId}/collections/users/data?page=${page}&limit=${limit}` ); + if (requestId !== latestUsersRequestId.current) return; setUsers(normalizeUsersResponse(usersRes.data)); setTotalRecords( usersRes.data?.data?.total || From 6aa66e5568dc438b9d93dc68956e6d0737a7cceb Mon Sep 17 00:00:00 2001 From: udaycodespace Date: Thu, 11 Jun 2026 20:01:45 +0530 Subject: [PATCH 3/7] fix(auth): address stale state handling in delete flow --- apps/web-dashboard/src/pages/Auth.jsx | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/apps/web-dashboard/src/pages/Auth.jsx b/apps/web-dashboard/src/pages/Auth.jsx index 77b7d101..35299456 100644 --- a/apps/web-dashboard/src/pages/Auth.jsx +++ b/apps/web-dashboard/src/pages/Auth.jsx @@ -170,10 +170,19 @@ export default function Auth() { if (!confirm('Delete this user? This cannot be undone.')) return; try { await api.delete(`/api/projects/${projectId}/collections/users/data/${userId}`); - const nextUsers = normalizeUsersResponse(users).filter(u => u._id !== userId); - setUsers(nextUsers); + setUsers(prevUsers => { + const nextUsers = normalizeUsersResponse(prevUsers).filter( + u => u._id !== userId + ); + + if (nextUsers.length === 0) { + setPage(prevPage => (prevPage > 1 ? prevPage - 1 : prevPage)); + } + + return nextUsers; + }); + setTotalRecords(prev => Math.max(prev - 1, 0)); - if (nextUsers.length === 0 && page > 1) setPage(page - 1); toast.success('User deleted'); } catch (err) { toast.error(err.response?.data?.message || err.response?.data?.error || 'Failed to delete user'); From e9175099fa8b591590f9873cddd336aa9161d429 Mon Sep 17 00:00:00 2001 From: udaycodespace Date: Thu, 11 Jun 2026 21:00:55 +0530 Subject: [PATCH 4/7] feat(auth): add admin user pagination endpoints --- .../src/controllers/userAuth.controller.js | 49 +++++++++++++++++++ apps/dashboard-api/src/routes/projects.js | 4 +- apps/web-dashboard/src/pages/Auth.jsx | 6 +-- 3 files changed, 55 insertions(+), 4 deletions(-) diff --git a/apps/dashboard-api/src/controllers/userAuth.controller.js b/apps/dashboard-api/src/controllers/userAuth.controller.js index 90ede0a3..70334f71 100644 --- a/apps/dashboard-api/src/controllers/userAuth.controller.js +++ b/apps/dashboard-api/src/controllers/userAuth.controller.js @@ -238,6 +238,55 @@ module.exports.createAdminUser = async (req, res) => { } } +module.exports.listAdminUsers = async (req, res) => { + try { + const project = req.project; + const usersColConfig = project.collections.find(c => c.name === 'users'); + if (!usersColConfig) return res.status(404).json({ error: "Auth collection not found" }); + + const page = Math.max(1, parseInt(req.query.page, 10) || 1); + const limit = Math.max(1, Math.min(parseInt(req.query.limit, 10) || 50, 100)); + const skip = (page - 1) * limit; + + const connection = await getConnection(project._id); + const Model = getCompiledModel(connection, usersColConfig, project._id, project.resources.db.isExternal); + + const [items, total] = await Promise.all([ + Model.find({}, { password: 0 }).sort({ createdAt: -1 }).skip(skip).limit(limit).lean(), + Model.countDocuments() + ]); + + res.json({ + items, + total, + page, + limit + }); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}; + +module.exports.deleteAdminUser = async (req, res) => { + try { + const project = req.project; + const { userId } = req.params; + + const usersColConfig = project.collections.find(c => c.name === 'users'); + if (!usersColConfig) return res.status(404).json({ error: "Auth collection not found" }); + + const connection = await getConnection(project._id); + const Model = getCompiledModel(connection, usersColConfig, project._id, project.resources.db.isExternal); + + const result = await Model.deleteOne({ _id: new mongoose.Types.ObjectId(userId) }); + if (result.deletedCount === 0) return res.status(404).json({ error: "User not found" }); + + res.json({ message: "User deleted successfully" }); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}; + // PATCH REQ FOR ADMIN RESET PASSWORD module.exports.resetPassword = async (req, res) => { try { diff --git a/apps/dashboard-api/src/routes/projects.js b/apps/dashboard-api/src/routes/projects.js index 0e42ba92..06c8d4cf 100644 --- a/apps/dashboard-api/src/routes/projects.js +++ b/apps/dashboard-api/src/routes/projects.js @@ -48,7 +48,7 @@ const { sendMarketingBroadcast } = require("../controllers/project.controller"); -const { createAdminUser, resetPassword, getUserDetails, updateAdminUser, listUserSessions, revokeUserSession } = require('../controllers/userAuth.controller'); +const { createAdminUser, resetPassword, getUserDetails, updateAdminUser, listAdminUsers, deleteAdminUser, listUserSessions, revokeUserSession } = require('../controllers/userAuth.controller'); const exportController = require('../controllers/dbExport.controller'); @@ -146,8 +146,10 @@ router.patch('/:projectId/collections/:collectionName/rls', authMiddleware, veri router.post('/:projectId/admin/users', authMiddleware, loadProjectForAdmin, checkAuthEnabled, createAdminUser); router.patch('/:projectId/admin/users/:userId/password', authMiddleware, loadProjectForAdmin, checkAuthEnabled, resetPassword); +router.get('/:projectId/admin/users', authMiddleware, loadProjectForAdmin, checkAuthEnabled, listAdminUsers); router.get('/:projectId/admin/users/:userId', authMiddleware, loadProjectForAdmin, checkAuthEnabled, getUserDetails); router.put('/:projectId/admin/users/:userId', authMiddleware, loadProjectForAdmin, checkAuthEnabled, updateAdminUser); +router.delete('/:projectId/admin/users/:userId', authMiddleware, loadProjectForAdmin, checkAuthEnabled, deleteAdminUser); // SESSION MANAGEMENT (Admin) router.get('/:projectId/admin/users/:userId/sessions', authMiddleware, loadProjectForAdmin, checkAuthEnabled, listUserSessions); diff --git a/apps/web-dashboard/src/pages/Auth.jsx b/apps/web-dashboard/src/pages/Auth.jsx index 35299456..e899812f 100644 --- a/apps/web-dashboard/src/pages/Auth.jsx +++ b/apps/web-dashboard/src/pages/Auth.jsx @@ -83,7 +83,7 @@ export default function Auth() { if (projRes.data.isAuthEnabled) { const requestId = ++latestUsersRequestId.current; const usersRes = await api.get( - `/api/projects/${projectId}/collections/users/data?page=${page}&limit=${limit}` + `/api/projects/${projectId}/admin/users?page=${page}&limit=${limit}` ); if (!isMounted || requestId !== latestUsersRequestId.current) return; @@ -169,7 +169,7 @@ export default function Auth() { const handleDeleteUser = async (userId) => { if (!confirm('Delete this user? This cannot be undone.')) return; try { - await api.delete(`/api/projects/${projectId}/collections/users/data/${userId}`); + await api.delete(`/api/projects/${projectId}/admin/users/${userId}`); setUsers(prevUsers => { const nextUsers = normalizeUsersResponse(prevUsers).filter( u => u._id !== userId @@ -251,7 +251,7 @@ export default function Auth() { toast.success('User created successfully'); const requestId = ++latestUsersRequestId.current; const usersRes = await api.get( - `/api/projects/${projectId}/collections/users/data?page=${page}&limit=${limit}` + `/api/projects/${projectId}/admin/users?page=${page}&limit=${limit}` ); if (requestId !== latestUsersRequestId.current) return; From 11d31d72041f2e5841596a9ce51a355fb10e836f Mon Sep 17 00:00:00 2001 From: udaycodespace Date: Thu, 11 Jun 2026 21:07:05 +0530 Subject: [PATCH 5/7] test(auth): mock new admin user routes --- .../dashboard-api/src/__tests__/routes.projects.storage.test.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/dashboard-api/src/__tests__/routes.projects.storage.test.js b/apps/dashboard-api/src/__tests__/routes.projects.storage.test.js index a0d550a3..3cbc5c31 100644 --- a/apps/dashboard-api/src/__tests__/routes.projects.storage.test.js +++ b/apps/dashboard-api/src/__tests__/routes.projects.storage.test.js @@ -28,6 +28,8 @@ jest.mock('../controllers/userAuth.controller', () => ({ resetPassword: jest.fn((_req, res) => res.json({ ok: true })), getUserDetails: jest.fn((_req, res) => res.json({ ok: true })), updateAdminUser: jest.fn((_req, res) => res.json({ ok: true })), + listAdminUsers: jest.fn((_req, res) => res.json({ ok: true })), + deleteAdminUser: jest.fn((_req, res) => res.json({ ok: true })), listUserSessions: jest.fn((_req, res) => res.json({ ok: true })), revokeUserSession: jest.fn((_req, res) => res.json({ ok: true })), })); From 1c671a1a8f326e32bb89417b15edfc6fb1eb10a9 Mon Sep 17 00:00:00 2001 From: udaycodespace Date: Thu, 11 Jun 2026 21:15:04 +0530 Subject: [PATCH 6/7] fix(auth): align admin endpoints with API conventions --- .../src/controllers/userAuth.controller.js | 35 ++++++++++++------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/apps/dashboard-api/src/controllers/userAuth.controller.js b/apps/dashboard-api/src/controllers/userAuth.controller.js index 70334f71..53092bae 100644 --- a/apps/dashboard-api/src/controllers/userAuth.controller.js +++ b/apps/dashboard-api/src/controllers/userAuth.controller.js @@ -8,7 +8,7 @@ const { authEmailQueue } = require('@urbackend/common'); const { loginSchema, signupSchema, userSignupSchema, resetPasswordSchema, onlyEmailSchema, verifyOtpSchema, changePasswordSchema, sanitize } = require('@urbackend/common'); const { getConnection } = require('@urbackend/common'); const { getCompiledModel } = require('@urbackend/common'); -const { getUserActiveSessions, getRefreshSession, revokeSessionChain } = require('@urbackend/common'); +const { AppError, getUserActiveSessions, getRefreshSession, revokeSessionChain } = require('@urbackend/common'); const hasRequiredField = (usersColConfig, fieldKey) => { const model = usersColConfig?.model || []; @@ -238,11 +238,11 @@ module.exports.createAdminUser = async (req, res) => { } } -module.exports.listAdminUsers = async (req, res) => { +module.exports.listAdminUsers = async (req, res, next) => { try { const project = req.project; const usersColConfig = project.collections.find(c => c.name === 'users'); - if (!usersColConfig) return res.status(404).json({ error: "Auth collection not found" }); + if (!usersColConfig) return next(new AppError(404, "Auth collection not found")); const page = Math.max(1, parseInt(req.query.page, 10) || 1); const limit = Math.max(1, Math.min(parseInt(req.query.limit, 10) || 50, 100)); @@ -257,33 +257,42 @@ module.exports.listAdminUsers = async (req, res) => { ]); res.json({ - items, - total, - page, - limit + success: true, + data: { items, total, page, limit }, + message: "" }); } catch (err) { - res.status(500).json({ error: err.message }); + next(new AppError(500, "Failed to list admin users")); } }; -module.exports.deleteAdminUser = async (req, res) => { +module.exports.deleteAdminUser = async (req, res, next) => { try { const project = req.project; const { userId } = req.params; + if (!mongoose.Types.ObjectId.isValid(userId)) { + return next(new AppError(400, "Invalid user ID")); + } + const usersColConfig = project.collections.find(c => c.name === 'users'); - if (!usersColConfig) return res.status(404).json({ error: "Auth collection not found" }); + if (!usersColConfig) return next(new AppError(404, "Auth collection not found")); const connection = await getConnection(project._id); const Model = getCompiledModel(connection, usersColConfig, project._id, project.resources.db.isExternal); const result = await Model.deleteOne({ _id: new mongoose.Types.ObjectId(userId) }); - if (result.deletedCount === 0) return res.status(404).json({ error: "User not found" }); + if (result.deletedCount === 0) { + return next(new AppError(404, "User not found")); + } - res.json({ message: "User deleted successfully" }); + res.json({ + success: true, + data: null, + message: "User deleted successfully" + }); } catch (err) { - res.status(500).json({ error: err.message }); + next(new AppError(500, "Failed to delete admin user")); } }; From a7274537dedc60d5c8ba84ff23b87ec819f03820 Mon Sep 17 00:00:00 2001 From: udaycodespace Date: Sat, 13 Jun 2026 15:52:08 +0530 Subject: [PATCH 7/7] refactor(auth): use ApiResponse for admin user endpoints --- .../src/controllers/userAuth.controller.js | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/apps/dashboard-api/src/controllers/userAuth.controller.js b/apps/dashboard-api/src/controllers/userAuth.controller.js index 53092bae..8a1c0f7a 100644 --- a/apps/dashboard-api/src/controllers/userAuth.controller.js +++ b/apps/dashboard-api/src/controllers/userAuth.controller.js @@ -8,7 +8,13 @@ const { authEmailQueue } = require('@urbackend/common'); const { loginSchema, signupSchema, userSignupSchema, resetPasswordSchema, onlyEmailSchema, verifyOtpSchema, changePasswordSchema, sanitize } = require('@urbackend/common'); const { getConnection } = require('@urbackend/common'); const { getCompiledModel } = require('@urbackend/common'); -const { AppError, getUserActiveSessions, getRefreshSession, revokeSessionChain } = require('@urbackend/common'); +const { + AppError, + ApiResponse, + getUserActiveSessions, + getRefreshSession, + revokeSessionChain +} = require('@urbackend/common'); const hasRequiredField = (usersColConfig, fieldKey) => { const model = usersColConfig?.model || []; @@ -256,11 +262,10 @@ module.exports.listAdminUsers = async (req, res, next) => { Model.countDocuments() ]); - res.json({ - success: true, - data: { items, total, page, limit }, - message: "" - }); + return new ApiResponse( + { items, total, page, limit }, + "" + ).send(res); } catch (err) { next(new AppError(500, "Failed to list admin users")); } @@ -286,11 +291,10 @@ module.exports.deleteAdminUser = async (req, res, next) => { return next(new AppError(404, "User not found")); } - res.json({ - success: true, - data: null, - message: "User deleted successfully" - }); + return new ApiResponse( + null, + "User deleted successfully" + ).send(res); } catch (err) { next(new AppError(500, "Failed to delete admin user")); }