Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -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 })),
}));
Expand Down
60 changes: 59 additions & 1 deletion apps/dashboard-api/src/controllers/userAuth.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 || [];
Expand Down Expand Up @@ -238,6 +238,64 @@ module.exports.createAdminUser = 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 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));
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({
success: true,
data: { items, total, page, limit },
message: ""
});
Comment on lines +259 to +263

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Everything looks perfect.
but I think we also have success response class.
DOnt remember.
I think @Ayush4958 has created in earlier PR maybe?
pleae check if exist
then use it
else I will merge.

} catch (err) {
next(new AppError(500, "Failed to list admin users"));
}
};

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 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 next(new AppError(404, "User not found"));
}

res.json({
success: true,
data: null,
message: "User deleted successfully"
});
} catch (err) {
next(new AppError(500, "Failed to delete admin user"));
}
};

// PATCH REQ FOR ADMIN RESET PASSWORD
module.exports.resetPassword = async (req, res) => {
try {
Expand Down
4 changes: 3 additions & 1 deletion apps/dashboard-api/src/routes/projects.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Comment thread
coderabbitai[bot] marked this conversation as resolved.
const exportController = require('../controllers/dbExport.controller');

Expand Down Expand Up @@ -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);
Expand Down
59 changes: 53 additions & 6 deletions apps/web-dashboard/src/pages/Auth.jsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';
Expand All @@ -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);
Expand All @@ -32,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 },
Expand Down Expand Up @@ -76,16 +81,26 @@ 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 requestId = ++latestUsersRequestId.current;
const usersRes = await api.get(
`/api/projects/${projectId}/admin/users?page=${page}&limit=${limit}`
);

if (!isMounted || requestId !== latestUsersRequestId.current) return;
setUsers(normalizeUsersResponse(usersRes.data));
setTotalRecords(
usersRes.data?.data?.total ||
usersRes.data?.total ||
normalizeUsersResponse(usersRes.data).length
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}
} catch { toast.error("Failed to load auth details"); }
finally { if (isMounted) setLoading(false); }
};
fetchData();
return () => { isMounted = false; };
}, [projectId]);
}, [projectId, page, limit]);

const handleEnableAuth = async () => {
if (!hasUserCollection) return toast.error("Please create a 'users' collection first.");
Expand Down Expand Up @@ -154,8 +169,20 @@ 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}`);
setUsers(prev => normalizeUsersResponse(prev).filter(u => u._id !== userId));
await api.delete(`/api/projects/${projectId}/admin/users/${userId}`);
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));
toast.success('User deleted');
} catch (err) {
toast.error(err.response?.data?.message || err.response?.data?.error || 'Failed to delete user');
Expand Down Expand Up @@ -222,8 +249,18 @@ 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 requestId = ++latestUsersRequestId.current;
const usersRes = await api.get(
`/api/projects/${projectId}/admin/users?page=${page}&limit=${limit}`
);

if (requestId !== latestUsersRequestId.current) return;
setUsers(normalizeUsersResponse(usersRes.data));
setTotalRecords(
usersRes.data?.data?.total ||
usersRes.data?.total ||
normalizeUsersResponse(usersRes.data).length
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
setIsAddModalOpen(false);
setEditingUser(null);
Expand Down Expand Up @@ -295,6 +332,16 @@ export default function Auth() {
onResetPassword={(u) => { setResetPasswordUser(u); setNewPassword(''); }}
onDelete={handleDeleteUser}
/>
<Pagination
total={totalRecords}
page={page}
limit={limit}
onPageChange={(p) => setPage(p)}
onLimitChange={(newLimit) => {
setLimit(newLimit);
setPage(1);
}}
/>
</div>
</div>

Expand Down
Loading