feat(activation): wire notifications into enrollment/auth, real password-reset delivery
- integration-routes.js: accepts student.contact_email, calls notifyForEnrollment on a genuinely new enrollment only (never on an idempotent replay — inherits Phase 5B's proven enrollment-level idempotency, no separate notification dedup needed). - admin-routes.js: create-student/convert/reset-password now enqueue the corresponding notification using the SAME token already minted synchronously (never mints twice, which would silently invalidate the token just handed back in the response). Fixed a real bug: request-password-reset would mint a reset token for a never-activated student, which makes no sense and would confuse them — now falls through to the generic anti-enumeration response. - admin-programs-routes.js: manual enrollment triggers the same notification decision as the website path. - student-auth-routes.js: /activate records a metric; /change-password and /reset-password both queue a password_changed security notice; /request-password-reset now actually delivers email (production still never returns the token — unchanged anti-enumeration/isProduction gating, just now with a real send behind it). - rbac.js: requireStudentRelationshipAccess/checkStudentRelationshipAccess — the notification visibility rule (cohort-shared or an enrollment they created), reusable beyond just enrollments. - server.js: wires notification-routes.js and starts the notification worker in main(). - docker-compose.dev.yml: PUBLIC_APP_ORIGIN, EMAIL_FROM_NAME, and a parametrized COOKIE_SECURE (was hard-commented-out; needed so e2e-phase5c.js can actually prove production gating).
This commit is contained in:
@@ -179,6 +179,38 @@ module.exports = function createRbac(pool) {
|
||||
};
|
||||
}
|
||||
|
||||
// Phase 5C §16 — same documented rule as requireEnrollmentAccess below, keyed by student_id
|
||||
// directly instead of resolving through one enrollment first: an instructor may see/manage a
|
||||
// student's notifications if they share a cohort with that student OR they created at least
|
||||
// one of that student's enrollments — otherwise 403, never blanket visibility into every
|
||||
// self-paced student's notifications ("Do not grant global notification visibility to every
|
||||
// instructor," §16). Used by the per-student notification list and the resend-activation/
|
||||
// send-enrollment-confirmation/send-password-reset admin actions.
|
||||
async function checkStudentRelationshipAccess(req, res, studentId) {
|
||||
const instructor = req.instructor;
|
||||
if (['owner', 'admin'].includes(instructor.role)) return true;
|
||||
const { rowCount } = await pool.query(
|
||||
`SELECT 1 FROM cohort_enrollments e JOIN cohort_instructors ci ON ci.cohort_id = e.cohort_id
|
||||
WHERE e.student_id = $1 AND e.enrollment_status = 'enrolled' AND ci.instructor_id = $2
|
||||
UNION SELECT 1 FROM enrollments WHERE student_id = $1 AND created_by = $2`,
|
||||
[studentId, instructor.instructor_id]
|
||||
);
|
||||
if (!rowCount) {
|
||||
await recordAudit(pool, {
|
||||
actorType: 'instructor', actorId: instructor.instructor_id, action: 'authz_denied',
|
||||
resourceType: 'student', resourceId: studentId, result: 'failure', sourceIp: req.ip,
|
||||
});
|
||||
res.status(403).json({ error: 'forbidden' });
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
function requireStudentRelationshipAccess(studentIdParam = 'id') {
|
||||
return async (req, res, next) => {
|
||||
if (await checkStudentRelationshipAccess(req, res, req.params[studentIdParam])) next();
|
||||
};
|
||||
}
|
||||
|
||||
// Phase 5B §11 — a self-paced enrollment may have NO cohort at all, so
|
||||
// requireStudentCohortAccess (which requires a cohort_enrollments row) can't gate it. The
|
||||
// documented safe rule for a plain instructor: they may see/manage an enrollment if EITHER
|
||||
@@ -216,6 +248,6 @@ module.exports = function createRbac(pool) {
|
||||
requireCohortAccess, requireStudentCohortAccess,
|
||||
requireCohortWriteAccess, requireStudentCohortWriteAccess,
|
||||
requireClassroomAccess, requireClassroomWriteAccess, requireHelpRequestWriteAccess,
|
||||
requireEnrollmentAccess,
|
||||
requireEnrollmentAccess, requireStudentRelationshipAccess,
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user