From 2b33c3c0af66f2b093a69dc6a4f6f2b0f4763dbb Mon Sep 17 00:00:00 2001 From: Shiva Shankara Vara Prasad Date: Sun, 27 Sep 2026 11:47:39 +0530 Subject: [PATCH 01/22] harden TPO review decisions and surface review history --- backend/controllers/adminController.js | 66 +++++++++++++++++++++++++- 1 file changed, 64 insertions(+), 2 deletions(-) diff --git a/backend/controllers/adminController.js b/backend/controllers/adminController.js index 8afb7b47..645a883e 100644 --- a/backend/controllers/adminController.js +++ b/backend/controllers/adminController.js @@ -79,6 +79,29 @@ export async function getPendingQueue(req, res) { (u) => !pendingCollegeRequesterIds.has(u._id.toString()) ); + const reviewHistoryByUser = new Map(); + if (individualTpoRequests.length) { + const reviews = await TpoVerificationReview.find({ + userId: { $in: individualTpoRequests.map((u) => u._id) }, + }) + .sort({ reviewedAt: -1 }) + .lean(); + + for (const review of reviews) { + const key = review.userId.toString(); + const history = reviewHistoryByUser.get(key) || []; + if (history.length < 5) { + history.push({ + decision: review.decision, + decisionReason: review.decisionReason, + reviewedAt: review.reviewedAt, + reviewedBy: review.reviewedBy, + }); + reviewHistoryByUser.set(key, history); + } + } + } + const studentColleges = pendingColleges.filter( (c) => c.submittedByRole === "student" || c.submittedByRole === "auto" ); @@ -128,6 +151,7 @@ export async function getPendingQueue(req, res) { verificationStatus: u.tpoVerification?.status || "pending", additionalEvidenceRecommended: (u.tpoVerification?.emailRoleSignal || "unknown") !== "staff_candidate", evidence: u.tpoVerification?.evidence || [], + reviewHistory: reviewHistoryByUser.get(u._id.toString()) || [], reviewTarget: "user", })), ], @@ -1027,11 +1051,42 @@ async function resolvePendingTpoCollege(user) { return College.findByDomain(domain); } +function getReviewDecisionReason(req, { required = false } = {}) { + const raw = req.body?.decisionReason; + if (raw == null) { + if (required) { + return { error: "A decision reason is required when rejecting a TPO verification request." }; + } + return { value: null }; + } + + const value = String(raw).trim(); + if (!value) { + if (required) { + return { error: "A decision reason is required when rejecting a TPO verification request." }; + } + return { value: null }; + } + + if (value.length > 1000) { + return { error: "Decision reason must be at most 1000 characters." }; + } + + return { value }; +} + export async function approveTpoUser(req, res) { try { const user = await User.findById(req.params.userId); if (!user || user.role !== "tpo") return res.status(404).json({ error: "TPO verification request not found." }); if (user.tpoProfile?.verified) return res.json({ success: true, alreadyVerified: true }); + if (user.tpoVerification?.status !== "pending") { + return res.status(409).json({ error: "Only a pending TPO verification request can be approved." }); + } + + const reasonResult = getReviewDecisionReason(req); + if (reasonResult.error) return res.status(400).json({ error: reasonResult.error }); + const decisionReason = reasonResult.value || "Approved through individual TPO verification."; const college = await resolvePendingTpoCollege(user); if (!college || college.status !== "verified") { @@ -1053,7 +1108,7 @@ export async function approveTpoUser(req, res) { emailRoleSignal: user.tpoVerification?.emailRoleSignal || "unknown", evidence: user.tpoVerification?.evidence || [], decision: "approved", - decisionReason: "Approved through individual TPO verification.", + decisionReason, reviewedBy: req.actingAdminDoc?._id || req.userDoc?._id, reviewedAt: now, }); @@ -1091,6 +1146,13 @@ export async function rejectTpoUser(req, res) { try { const user = await User.findById(req.params.userId); if (!user || user.role !== "tpo") return res.status(404).json({ error: "TPO verification request not found." }); + if (user.tpoVerification?.status !== "pending") { + return res.status(409).json({ error: "Only a pending TPO verification request can be rejected." }); + } + + const reasonResult = getReviewDecisionReason(req, { required: true }); + if (reasonResult.error) return res.status(400).json({ error: reasonResult.error }); + const decisionReason = reasonResult.value; const college = await resolvePendingTpoCollege(user); const reviewerId = req.actingAdminDoc?._id || req.userDoc?._id; @@ -1105,7 +1167,7 @@ export async function rejectTpoUser(req, res) { emailRoleSignal: user.tpoVerification?.emailRoleSignal || "unknown", evidence: user.tpoVerification?.evidence || [], decision: "rejected", - decisionReason: "Individual TPO verification request rejected.", + decisionReason, reviewedBy: reviewerId, reviewedAt: now, }); From c7bdfc1b938c94827998f2f3db888cbea15cefc8 Mon Sep 17 00:00:00 2001 From: Shiva Shankara Vara Prasad Date: Sun, 27 Sep 2026 11:48:09 +0530 Subject: [PATCH 02/22] test TPO review decision hardening --- backend/controllers/adminController.test.js | 142 +++++++++++++++++++- 1 file changed, 141 insertions(+), 1 deletion(-) diff --git a/backend/controllers/adminController.test.js b/backend/controllers/adminController.test.js index 1f55d5c9..59461ca4 100644 --- a/backend/controllers/adminController.test.js +++ b/backend/controllers/adminController.test.js @@ -1,7 +1,10 @@ import { describe, expect, it, vi, beforeEach } from "vitest"; vi.mock("../models/TpoVerificationReview.js", () => ({ - default: { create: vi.fn().mockResolvedValue({}) }, + default: { + create: vi.fn().mockResolvedValue({}), + find: vi.fn(), + }, })); vi.mock("../models/College.js", () => ({ default: { @@ -245,6 +248,143 @@ describe("adminController", () => { }); }); + describe("individual TPO verification decisions", () => { + function makePendingTpo(overrides = {}) { + return makeUser({ + _id: "tpo1", + firebaseUid: "fb-tpo1", + email: "staff@mit.edu", + role: "tpo", + roles: ["student", "tpo"], + tpoProfile: { + collegeDomain: "mit.edu", + collegeName: "MIT", + verified: false, + requestedAt: "request-time", + verifiedAt: null, + }, + tpoVerification: { + status: "pending", + emailRoleSignal: "staff_candidate", + submittedEmail: "staff@mit.edu", + submittedAt: "request-time", + evidence: [], + }, + ...overrides, + }); + } + + it("approves only a pending applicant, records the review reason, and claims primary after approval", async () => { + const user = makePendingTpo(); + const college = { + _id: "c1", + name: "MIT", + status: "verified", + domains: ["mit.edu"], + }; + const admin = makeAdmin(); + User.findById.mockResolvedValueOnce(user); + College.findByDomain.mockResolvedValueOnce(college); + claimPrimaryIfNone.mockResolvedValueOnce(true); + + await approveTpoUser({ + params: { userId: "tpo1" }, + body: { decisionReason: "Staff identity matched the submitted institutional evidence." }, + userDoc: admin, + }, res); + + expect(user.tpoProfile.verified).toBe(true); + expect(user.tpoVerification.status).toBe("approved"); + expect(user.save).toHaveBeenCalledOnce(); + expect(TpoVerificationReview.create).toHaveBeenCalledWith( + expect.objectContaining({ + userId: "tpo1", + collegeId: "c1", + decision: "approved", + decisionReason: "Staff identity matched the submitted institutional evidence.", + reviewedBy: "admin1", + }) + ); + expect(claimPrimaryIfNone).toHaveBeenCalledWith("c1", "tpo1"); + expect(recordAdminAction).toHaveBeenCalledWith( + expect.objectContaining({ action: "tpo.user.approve", targetType: "User", targetId: "tpo1" }) + ); + expect(res.json).toHaveBeenCalledWith({ success: true }); + }); + + it("rejects without mutating the applicant when a non-pending request is reviewed", async () => { + const user = makePendingTpo({ + tpoVerification: { + status: "rejected", + emailRoleSignal: "unknown", + submittedEmail: "staff@mit.edu", + evidence: [], + }, + }); + User.findById.mockResolvedValueOnce(user); + + await rejectTpoUser({ params: { userId: "tpo1" }, body: { decisionReason: "Insufficient evidence." }, userDoc: makeAdmin() }, res); + + expect(res.status).toHaveBeenCalledWith(409); + expect(user.save).not.toHaveBeenCalled(); + expect(TpoVerificationReview.create).not.toHaveBeenCalled(); + }); + + it("requires a reason when rejecting a pending applicant", async () => { + const user = makePendingTpo(); + User.findById.mockResolvedValueOnce(user); + + await rejectTpoUser({ params: { userId: "tpo1" }, body: {}, userDoc: makeAdmin() }, res); + + expect(res.status).toHaveBeenCalledWith(400); + expect(user.save).not.toHaveBeenCalled(); + expect(TpoVerificationReview.create).not.toHaveBeenCalled(); + }); + + it("rejects a pending applicant and preserves the reason in the immutable review", async () => { + const user = makePendingTpo(); + const college = { _id: "c1", name: "MIT", status: "verified", domains: ["mit.edu"] }; + const admin = makeAdmin(); + User.findById.mockResolvedValueOnce(user); + College.findByDomain.mockResolvedValueOnce(college); + + await rejectTpoUser({ + params: { userId: "tpo1" }, + body: { decisionReason: "The submitted staff evidence could not be verified." }, + userDoc: admin, + }, res); + + expect(user.role).toBe("student"); + expect(user.roles).toEqual(["student"]); + expect(user.tpoVerification.status).toBe("rejected"); + expect(TpoVerificationReview.create).toHaveBeenCalledWith( + expect.objectContaining({ + userId: "tpo1", + collegeId: "c1", + decision: "rejected", + decisionReason: "The submitted staff evidence could not be verified.", + reviewedBy: "admin1", + }) + ); + expect(invalidateCachedUserByFirebaseUid).toHaveBeenCalledWith("fb-tpo1"); + expect(res.json).toHaveBeenCalledWith({ success: true }); + }); + + it("does not allow an oversized decision reason", async () => { + const user = makePendingTpo(); + User.findById.mockResolvedValueOnce(user); + + await rejectTpoUser({ + params: { userId: "tpo1" }, + body: { decisionReason: "x".repeat(1001) }, + userDoc: makeAdmin(), + }, res); + + expect(res.status).toHaveBeenCalledWith(400); + expect(user.save).not.toHaveBeenCalled(); + }); + }); + describe("approveTpo", () => { it("verifies the college without bulk-authorizing individual TPOs", async () => { const college = { From 1f260e039b58af9ac89b04df06865eb3328fbad0 Mon Sep 17 00:00:00 2001 From: Shiva Shankara Vara Prasad Date: Sun, 27 Sep 2026 11:48:17 +0530 Subject: [PATCH 03/22] stabilize TPO review queue test mocks --- backend/controllers/adminController.test.js | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/controllers/adminController.test.js b/backend/controllers/adminController.test.js index 59461ca4..bb612da2 100644 --- a/backend/controllers/adminController.test.js +++ b/backend/controllers/adminController.test.js @@ -146,6 +146,7 @@ describe("adminController", () => { beforeEach(() => { vi.clearAllMocks(); + TpoVerificationReview.find.mockReturnValue(chainableQuery([])); res = mockRes(); }); From b3f7fed3fffb55f6dfa7d54d746880bc8684a56c Mon Sep 17 00:00:00 2001 From: Shiva Shankara Vara Prasad Date: Sun, 27 Sep 2026 11:48:36 +0530 Subject: [PATCH 04/22] allow custom review content in confirm dialog --- src/components/ui/ConfirmDialog.jsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/ui/ConfirmDialog.jsx b/src/components/ui/ConfirmDialog.jsx index 989cd973..5c534114 100644 --- a/src/components/ui/ConfirmDialog.jsx +++ b/src/components/ui/ConfirmDialog.jsx @@ -55,6 +55,7 @@ export default function ConfirmDialog({ loading = false, onConfirm, onCancel, + children, }) { const panelRef = useRef(null); const triggerRef = useRef(null); @@ -131,6 +132,7 @@ export default function ConfirmDialog({ {title} {description &&

{description}

} + {children}
- +
+
+
+

{title}

+

{subtitle}

+ {meta &&

{meta}

} + {signal && ( +

+ Email signal: {signal.replaceAll("_", " ")} + {evidenceHint ? " · additional evidence recommended" : ""} +

+ )} + {Array.isArray(evidence) && evidence.length > 0 && ( +

+ Evidence: {evidence.map((item) => item.label).join(", ")} +

+ )} +
+ +
+ {hasDetails && ( + + )} + + +
+ {showDetails && ( +
+ {Array.isArray(evidence) && evidence.length > 0 && ( +
+

Submitted evidence

+
+ {evidence.map((item, index) => ( +
+

{item.label}

+

+ {item.kind.replaceAll("_", " ")} + {item.reference ? ` · ${item.reference}` : ""} +

+ {item.note &&

{item.note}

} +
+ ))} +
+
+ )} + + {Array.isArray(reviewHistory) && reviewHistory.length > 0 && ( +
+

Previous review history

+
+ {reviewHistory.map((review, index) => ( +
+

+ {review.decision} + {review.reviewedAt ? ` · ${new Date(review.reviewedAt).toLocaleString()}` : ""} +

+ {review.decisionReason && ( +

{review.decisionReason}

+ )} +
+ ))} +
+
+ )} +
+ )} + {confirmingReject && ( { + const reason = rejectReason.trim(); + if (isTpoReview && !reason) return; setConfirmingReject(false); - onReject(); + onReject(reason || undefined); + setRejectReason(""); }} - onCancel={() => setConfirmingReject(false)} - /> + onCancel={closeRejectDialog} + > + {isTpoReview && ( +
+ +