From c024adb023017338eecd969347373c128af2fafc Mon Sep 17 00:00:00 2001 From: evan Date: Mon, 13 Jul 2026 04:24:56 -0700 Subject: [PATCH 1/9] Mailer.js has way less lines now --- .../email_templates/passwordReset.js | 38 +++--- api/cloud_api/email_templates/verification.js | 42 +++---- api/cloud_api/routes/Mailer.js | 118 ++++++++---------- 3 files changed, 81 insertions(+), 117 deletions(-) diff --git a/api/cloud_api/email_templates/passwordReset.js b/api/cloud_api/email_templates/passwordReset.js index 4aec1746c..09dffc76a 100644 --- a/api/cloud_api/email_templates/passwordReset.js +++ b/api/cloud_api/email_templates/passwordReset.js @@ -1,28 +1,20 @@ const generateHashedId = require('../util/auth').generateHashedId; -function passwordReset(user, resetToken, recipient) { - return new Promise((resolve, reject) => { - generateHashedId(recipient) - .then(hashedId => { - const url = - process.env.VERIFICATION_BASE_URL || 'http://localhost:3000'; - const resetLink = - `${url}/reset?id=${hashedId}&resetToken=${resetToken}`; - return resolve({ - from: user, - to: recipient, - subject: 'Reset Password for SCE', - generateTextFromHTML: true, - html: ` - Hi,
-

Click the link below to reset your password. It will expire in 24 hours.

- Reset Password - ` - }); - }) - .catch(error => { - reject(error); - }); +function passwordReset(hashedId, user, resetToken, recipient) { + const url = + process.env.VERIFICATION_BASE_URL || 'http://localhost:3000'; + const resetLink = + `${url}/reset?id=${hashedId}&resetToken=${resetToken}`; + return resolve({ + from: user, + to: recipient, + subject: 'Reset Password for SCE', + generateTextFromHTML: true, + html: ` + Hi,
+

Click the link below to reset your password. It will expire in 24 hours.

+ Reset Password + ` }); } diff --git a/api/cloud_api/email_templates/verification.js b/api/cloud_api/email_templates/verification.js index 94149b1ba..39a9bd697 100644 --- a/api/cloud_api/email_templates/verification.js +++ b/api/cloud_api/email_templates/verification.js @@ -1,30 +1,22 @@ const generateHashedId = require('../util/auth').generateHashedId; -function verification(user, recipient, name) { - return new Promise((resolve, reject) => { - generateHashedId(recipient) - .then(hashedId => { - const url = - process.env.VERIFICATION_BASE_URL || 'http://localhost:3000'; - const verifyLink = - `${url}/verify?id=${hashedId}&user=${recipient}`; - return resolve({ - from: user, - to: recipient, - subject: 'Please verify your email address', - generateTextFromHTML: true, - html: ` - Hi ${name || ''},
-

Thanks for signing up! - Please verify your email by clicking below.

- Verify Email - ` - }); - }) - .catch(error => { - reject(error); - }); - }); +function verification(hashedId, user, recipient, name) { + const url = + process.env.VERIFICATION_BASE_URL || 'http://localhost:3000'; + const verifyLink = + `${url}/verify?id=${hashedId}&user=${recipient}`; + return { + from: user, + to: recipient, + subject: 'Please verify your email address', + generateTextFromHTML: true, + html: ` + Hi ${name || ''},
+

Thanks for signing up! + Please verify your email by clicking below.

+ Verify Email + ` + }; } module.exports = { verification }; diff --git a/api/cloud_api/routes/Mailer.js b/api/cloud_api/routes/Mailer.js index 605359f29..ff441a7ae 100644 --- a/api/cloud_api/routes/Mailer.js +++ b/api/cloud_api/routes/Mailer.js @@ -3,7 +3,6 @@ const router = express.Router(); const { SceGoogleApiHandler } = require('../util/SceGoogleApiHandler'); const { verification } = require('../email_templates/verification'); const { passwordReset } = require('../email_templates/passwordReset'); -const { blastEmail } = require('../email_templates/blastEmail'); const { unsubscribeEmail } = require('../email_templates/unsubscribeEmail'); const { membershipConfirmationCode } = require('../email_templates/membershipConfirmationCode'); const { @@ -15,16 +14,12 @@ const logger = require('../../util/logger'); const { googleApiKeys } = require('../../config/config.json'); const { USER, ENABLED } = googleApiKeys; const { MetricsHandler } = require('../../util/metrics'); +const generateHashedId = require('../util/auth').generateHashedId; -// Routing post /sendVerificationEmail calls the sendEmail function -// and sends the verification email with the verification email template -router.post('/sendVerificationEmail', async (req, res) => { - if (!ENABLED && process.env.NODE_ENV !== 'test') { - return res.sendStatus(OK); - } +async function maybeRefreshTokenFromGcp() { + const apiHandler = new SceGoogleApiHandler(scopes, pathToToken); const scopes = ['https://mail.google.com/']; const pathToToken = __dirname + '/../../config/token.json'; - const apiHandler = new SceGoogleApiHandler(scopes, pathToToken); const tokenJson = await apiHandler.checkIfTokenFileExists(); if (tokenJson) { @@ -40,25 +35,34 @@ router.post('/sendVerificationEmail', async (req, res) => { logger.warn('getting new token! ', { tokenJson }); apiHandler.getNewToken(); } +} +// Routing post /sendVerificationEmail calls the sendEmail function +// and sends the verification email with the verification email template +router.post('/sendVerificationEmail', async (req, res) => { + if (!ENABLED && process.env.NODE_ENV !== 'test') { + return res.sendStatus(OK); + } - await verification(USER, req.body.recipientEmail, req.body.recipientName) - .then((template) => { - apiHandler - .sendEmail(template) - .then((_) => { - res.sendStatus(OK); - MetricsHandler.emailSent.inc({ type: 'verification' }); - }) - .catch((err) => { - logger.error('unable to send verification email:', err); - res.sendStatus(BAD_REQUEST); - }); - }) - .catch((err) => { - logger.error('unable to generate verification template:', err); - res.sendStatus(BAD_REQUEST); - }); + await maybeRefreshTokenFromGcp(); + + const apiHandler = new SceGoogleApiHandler(scopes, pathToToken); + + try { + const hashedId = await generateHashedId(req.body.recipientEmail); + } catch(e) { + logger.error('unable to generate verification template:', err); + return res.sendStatus(BAD_REQUEST); + } + const template = verification(USER, req.body.recipientEmail, req.body.recipientName); + try { + await apiHandler.sendEmail(template); + MetricsHandler.emailSent.inc({ type: 'verification' }); + return res.sendStatus(OK); + } catch(e) { + logger.error('unable to send verification email:', err); + res.sendStatus(BAD_REQUEST); + } }); // Routing post /sendPasswordReset calls the sendEmail function @@ -67,39 +71,26 @@ router.post('/sendPasswordReset', async (req, res) => { if (!ENABLED && process.env.NODE_ENV !== 'test') { return res.sendStatus(OK); } - const scopes = ['https://mail.google.com/']; - const pathToToken = __dirname + '/../../config/token.json'; + + await maybeRefreshTokenFromGcp(); + const apiHandler = new SceGoogleApiHandler(scopes, pathToToken); - const tokenJson = await apiHandler.checkIfTokenFileExists(); - if (tokenJson) { - if (apiHandler.checkIfTokenIsExpired(tokenJson)) { - logger.warn('refreshing token'); - MetricsHandler.gcpRefreshTokenLastUpdated.set(Math.floor(Date.now() / 1000)); - apiHandler.refreshToken(); - } - } else { - logger.warn('getting new token! ', { tokenJson }); - apiHandler.getNewToken(); + try { + const hashedId = await generateHashedId(req.body.recipientEmail); + } catch(err) { + logger.error('unable to send password reset email:', err); + return res.sendStatus(BAD_REQUEST); + } + const template = passwordReset(USER, req.body.resetToken, req.body.recipientEmail); + try { + await apiHandler.sendEmail(template); + MetricsHandler.emailSent.inc({ type: 'verification' }); + return res.sendStatus(OK); + } catch(err) { + logger.error('unable to generate password reset template:', err); + res.sendStatus(BAD_REQUEST); } - - await passwordReset(USER, req.body.resetToken, req.body.recipientEmail) - .then((template) => { - apiHandler - .sendEmail(template) - .then((_) => { - res.sendStatus(OK); - MetricsHandler.emailSent.inc({ type: 'passwordReset' }); - }) - .catch((err) => { - logger.error('unable to send password reset email:', err); - res.sendStatus(BAD_REQUEST); - }); - }) - .catch((err) => { - logger.error('unable to generate password reset template:', err); - res.sendStatus(BAD_REQUEST); - }); }); // Routing post /sendUnsubscribeEmail calls the unsubscribeEmail function @@ -140,21 +131,10 @@ router.post('/sendMembershipConfirmationCode', async (req, res) => { if (!ENABLED && process.env.NODE_ENV !== 'test') { return res.sendStatus(OK); } - const scopes = ['https://mail.google.com/']; - const pathToToken = __dirname + '/../../config/token.json'; - const apiHandler = new SceGoogleApiHandler(scopes, pathToToken); - const tokenJson = await apiHandler.checkIfTokenFileExists(); + + await maybeRefreshTokenFromGcp(); - if (tokenJson) { - if (apiHandler.checkIfTokenIsExpired(tokenJson)) { - logger.warn('refreshing token'); - MetricsHandler.gcpRefreshTokenLastUpdated.set(Math.floor(Date.now() / 1000)); - apiHandler.refreshToken(); - } - } else { - logger.warn('getting new token! ', { tokenJson }); - apiHandler.getNewToken(); - } + const apiHandler = new SceGoogleApiHandler(scopes, pathToToken); const { recipientEmail, confirmationCode } = req.body; From 11b30127aac789b81e20671e97e5d7628d13e9b6 Mon Sep 17 00:00:00 2001 From: evan Date: Sat, 25 Jul 2026 11:24:33 -0700 Subject: [PATCH 2/9] lint, define scopes at top of file --- .../email_templates/passwordReset.js | 4 ++-- api/cloud_api/routes/Mailer.js | 19 ++++++++++--------- api/main_endpoints/models/AuditLog.js | 2 +- api/main_endpoints/routes/Auth.js | 4 +++- src/Components/LinkifiedText/LinkifiedText.js | 7 ++----- src/Pages/ForgotPassword/ForgotPassword.js | 8 +++++--- 6 files changed, 23 insertions(+), 21 deletions(-) diff --git a/api/cloud_api/email_templates/passwordReset.js b/api/cloud_api/email_templates/passwordReset.js index 09dffc76a..f8e547f50 100644 --- a/api/cloud_api/email_templates/passwordReset.js +++ b/api/cloud_api/email_templates/passwordReset.js @@ -5,7 +5,7 @@ function passwordReset(hashedId, user, resetToken, recipient) { process.env.VERIFICATION_BASE_URL || 'http://localhost:3000'; const resetLink = `${url}/reset?id=${hashedId}&resetToken=${resetToken}`; - return resolve({ + return { from: user, to: recipient, subject: 'Reset Password for SCE', @@ -15,7 +15,7 @@ function passwordReset(hashedId, user, resetToken, recipient) {

Click the link below to reset your password. It will expire in 24 hours.

Reset Password ` - }); + }; } module.exports = { passwordReset }; diff --git a/api/cloud_api/routes/Mailer.js b/api/cloud_api/routes/Mailer.js index ff441a7ae..369e94baf 100644 --- a/api/cloud_api/routes/Mailer.js +++ b/api/cloud_api/routes/Mailer.js @@ -16,10 +16,11 @@ const { USER, ENABLED } = googleApiKeys; const { MetricsHandler } = require('../../util/metrics'); const generateHashedId = require('../util/auth').generateHashedId; +const scopes = ['https://mail.google.com/']; +const pathToToken = __dirname + '/../../config/token.json'; + async function maybeRefreshTokenFromGcp() { const apiHandler = new SceGoogleApiHandler(scopes, pathToToken); - const scopes = ['https://mail.google.com/']; - const pathToToken = __dirname + '/../../config/token.json'; const tokenJson = await apiHandler.checkIfTokenFileExists(); if (tokenJson) { @@ -48,19 +49,21 @@ router.post('/sendVerificationEmail', async (req, res) => { const apiHandler = new SceGoogleApiHandler(scopes, pathToToken); + let template = ''; try { const hashedId = await generateHashedId(req.body.recipientEmail); + template = verification(hashedId, USER, req.body.recipientEmail, req.body.recipientName); } catch(e) { logger.error('unable to generate verification template:', err); return res.sendStatus(BAD_REQUEST); } - const template = verification(USER, req.body.recipientEmail, req.body.recipientName); + try { await apiHandler.sendEmail(template); MetricsHandler.emailSent.inc({ type: 'verification' }); return res.sendStatus(OK); } catch(e) { - logger.error('unable to send verification email:', err); + logger.error('unable to send verification email:', e); res.sendStatus(BAD_REQUEST); } }); @@ -76,13 +79,14 @@ router.post('/sendPasswordReset', async (req, res) => { const apiHandler = new SceGoogleApiHandler(scopes, pathToToken); + let template = ''; try { const hashedId = await generateHashedId(req.body.recipientEmail); + template = passwordReset(hashedId, USER, req.body.resetToken, req.body.recipientEmail); } catch(err) { logger.error('unable to send password reset email:', err); return res.sendStatus(BAD_REQUEST); } - const template = passwordReset(USER, req.body.resetToken, req.body.recipientEmail); try { await apiHandler.sendEmail(template); MetricsHandler.emailSent.inc({ type: 'verification' }); @@ -102,9 +106,6 @@ router.post('/sendUnsubscribeEmail', async (req, res) => { return res.sendStatus(BAD_REQUEST); } - - const scopes = ['https://mail.google.com/']; - const pathToToken = __dirname + '/../../config/token.json'; const apiHandler = new SceGoogleApiHandler(scopes, pathToToken); for (let i = 0; i < req.body.users.length; i++) { (function(i) { @@ -131,7 +132,7 @@ router.post('/sendMembershipConfirmationCode', async (req, res) => { if (!ENABLED && process.env.NODE_ENV !== 'test') { return res.sendStatus(OK); } - + await maybeRefreshTokenFromGcp(); const apiHandler = new SceGoogleApiHandler(scopes, pathToToken); diff --git a/api/main_endpoints/models/AuditLog.js b/api/main_endpoints/models/AuditLog.js index ff4110742..2ba2191a0 100644 --- a/api/main_endpoints/models/AuditLog.js +++ b/api/main_endpoints/models/AuditLog.js @@ -26,7 +26,7 @@ const AuditLogSchema = new Schema( { timestamps: { createdAt: true, updatedAt: false } } ); -AuditLogSchema.index({ _id: 1, action: 1 }); +AuditLogSchema.index({ userId: 1, action: 1 }); AuditLogSchema.post('save', async function(doc) { const newDoc = await doc.constructor.findById(doc._id).populate('userId', 'firstName lastName email'); diff --git a/api/main_endpoints/routes/Auth.js b/api/main_endpoints/routes/Auth.js index f92555b97..f73899e5d 100644 --- a/api/main_endpoints/routes/Auth.js +++ b/api/main_endpoints/routes/Auth.js @@ -114,9 +114,11 @@ router.post('/sendPasswordReset', async (req, res) => { const passwordReset = new PasswordReset({ resetToken, userId: String(result._id), + // 24 hours in milliseconds, (86,400,000 ms) + expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000), }); - await passwordReset.save(); await sendPasswordReset(resetToken, req.body.email); + await passwordReset.save(); // create audit log for sending reset password email AuditLog.create({ diff --git a/src/Components/LinkifiedText/LinkifiedText.js b/src/Components/LinkifiedText/LinkifiedText.js index 5cc841d17..1e1904df3 100644 --- a/src/Components/LinkifiedText/LinkifiedText.js +++ b/src/Components/LinkifiedText/LinkifiedText.js @@ -1,4 +1,4 @@ -import Autolinker from 'autolinker'; +// import Autolinker from 'autolinker'; /** * Component that automatically converts URLs and email addresses in text into clickable links. @@ -12,10 +12,7 @@ export default function LinkifiedText({ children }) { return <>{children}; } - const matches = Autolinker.parse(children, { - urls: true, - email: true, - }); + const matches = []; if (matches.length === 0) { return <>{children}; diff --git a/src/Pages/ForgotPassword/ForgotPassword.js b/src/Pages/ForgotPassword/ForgotPassword.js index 0e8603aa9..b2214a658 100644 --- a/src/Pages/ForgotPassword/ForgotPassword.js +++ b/src/Pages/ForgotPassword/ForgotPassword.js @@ -21,7 +21,9 @@ export default function Login() { return; } setLoading(true); - captchaRef.reset(); + if (process.env.NODE_ENV === 'production') { + captchaRef.reset(); + } const resetStatus = await sendPasswordReset(email, captchaValue); if (resetStatus.error) { setMessage(resetStatus.error?.response?.data?.message || 'An error occurred. Please try again later.'); @@ -51,9 +53,9 @@ export default function Login() { setEmail(e.target.value)}/> -
+ {process.env.NODE_ENV === 'production' &&
-
+
} {message &&

Date: Sat, 25 Jul 2026 11:54:34 -0700 Subject: [PATCH 3/9] fix mail test --- api/cloud_api/routes/Mailer.js | 3 +-- test/api/Mailer.js | 14 +++++++------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/api/cloud_api/routes/Mailer.js b/api/cloud_api/routes/Mailer.js index 369e94baf..7481a3554 100644 --- a/api/cloud_api/routes/Mailer.js +++ b/api/cloud_api/routes/Mailer.js @@ -44,7 +44,6 @@ router.post('/sendVerificationEmail', async (req, res) => { if (!ENABLED && process.env.NODE_ENV !== 'test') { return res.sendStatus(OK); } - await maybeRefreshTokenFromGcp(); const apiHandler = new SceGoogleApiHandler(scopes, pathToToken); @@ -54,7 +53,7 @@ router.post('/sendVerificationEmail', async (req, res) => { const hashedId = await generateHashedId(req.body.recipientEmail); template = verification(hashedId, USER, req.body.recipientEmail, req.body.recipientName); } catch(e) { - logger.error('unable to generate verification template:', err); + logger.error('unable to generate verification template:', e); return res.sendStatus(BAD_REQUEST); } diff --git a/test/api/Mailer.js b/test/api/Mailer.js index 1422c7ffb..cb4c56ef3 100644 --- a/test/api/Mailer.js +++ b/test/api/Mailer.js @@ -8,8 +8,8 @@ const { OK, BAD_REQUEST } = constants.STATUS_CODES; const SceApiTester = require('../util/tools/SceApiTester'); const { SceGoogleApiHandler } = require('../../api/cloud_api/util/SceGoogleApiHandler'); -const verificationTemplate = - require('../../api/cloud_api/email_templates/verification'); +const authUtils = + require('../../api/cloud_api/util/auth'); let app = null; let test = null; @@ -22,10 +22,10 @@ chai.use(chaiHttp); describe('Mailer', () => { let sendEmailStub = null; - let verificationStub = null; + let generateHashedIdStub = null; before(done => { sendEmailStub = sandbox.stub(SceGoogleApiHandler.prototype, 'sendEmail'); - verificationStub = sandbox.stub(verificationTemplate, 'verification'); + generateHashedIdStub = sandbox.stub(authUtils, 'generateHashedId'); app = tools.initializeServer( __dirname + '/../../api/cloud_api/routes/Mailer.js'); test = new SceApiTester(app); @@ -34,7 +34,7 @@ describe('Mailer', () => { after(done => { if (sendEmailStub) sendEmailStub.restore(); - if (verificationStub) verificationStub.restore(); + if (generateHashedIdStub) generateHashedIdStub.restore(); sandbox.restore(); tools.terminateServer(done); }); @@ -47,7 +47,7 @@ describe('Mailer', () => { describe('/POST sendVerificationEmail', () => { it('Should return 200 when an email is successfully sent', async () => { sendEmailStub.resolves({}); - verificationStub.resolves({}); + generateHashedIdStub.resolves({}); const result = await test.sendPostRequest( '/api/Mailer/sendVerificationEmail', VALID_EMAIL_REQUEST); expect(result).to.have.status(OK); @@ -55,7 +55,7 @@ describe('Mailer', () => { it('Should return 400 when we cannot generate a hashed ID', async () => { sendEmailStub.resolves({}); - verificationStub.rejects({}); + generateHashedIdStub.rejects({}); const result = await test.sendPostRequest( '/api/Mailer/sendVerificationEmail', VALID_EMAIL_REQUEST); expect(result).to.have.status(BAD_REQUEST); From 99185e7b897b188606cd05a21bbb14affd63eb9f Mon Sep 17 00:00:00 2001 From: evan Date: Sat, 25 Jul 2026 11:55:59 -0700 Subject: [PATCH 4/9] undo linkified --- src/Components/LinkifiedText/LinkifiedText.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Components/LinkifiedText/LinkifiedText.js b/src/Components/LinkifiedText/LinkifiedText.js index 1e1904df3..5cc841d17 100644 --- a/src/Components/LinkifiedText/LinkifiedText.js +++ b/src/Components/LinkifiedText/LinkifiedText.js @@ -1,4 +1,4 @@ -// import Autolinker from 'autolinker'; +import Autolinker from 'autolinker'; /** * Component that automatically converts URLs and email addresses in text into clickable links. @@ -12,7 +12,10 @@ export default function LinkifiedText({ children }) { return <>{children}; } - const matches = []; + const matches = Autolinker.parse(children, { + urls: true, + email: true, + }); if (matches.length === 0) { return <>{children}; From 81d20593b00e32cdb1bd51d0460d5a796d191099 Mon Sep 17 00:00:00 2001 From: evan Date: Sat, 25 Jul 2026 12:16:05 -0700 Subject: [PATCH 5/9] i guess printer tests were broken --- api/main_endpoints/routes/Printer.js | 25 +++++++++++++++---------- test/api/Printer.js | 21 +++++++++++++++++++-- 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/api/main_endpoints/routes/Printer.js b/api/main_endpoints/routes/Printer.js index 9cd1daae9..9c5a03315 100644 --- a/api/main_endpoints/routes/Printer.js +++ b/api/main_endpoints/routes/Printer.js @@ -124,17 +124,22 @@ router.post('/sendPrintRequest', upload.single('chunk'), async (req, res) => { } try { - // full pdf can be sent to quasar no problem - const printRes = await axios.post(PRINTER_URL + '/print', data, { - headers: { - ...data.getHeaders(), - }, - maxContentLength: 1024 * 1024 * 150, // 150 mb - maxBodyLength: Infinity - }); + let printId = 'if this is the id we didnt print anything due to us thinking the environment was "test"'; + + if (process.env.NODE_ENV !== 'test') { - // { print_id: null | string } - const printId = printRes.data; + // full pdf can be sent to quasar no problem + const printRes = await axios.post(PRINTER_URL + '/print', data, { + headers: { + ...data.getHeaders(), + }, + maxContentLength: 1024 * 1024 * 150, // 150 mb + maxBodyLength: Infinity + }); + + // { print_id: null | string } + const printId = printRes.data; + } await cleanUpChunks(dir, id); res.status(OK).send(printId); diff --git a/test/api/Printer.js b/test/api/Printer.js index 7e285dc1a..33315e296 100644 --- a/test/api/Printer.js +++ b/test/api/Printer.js @@ -26,6 +26,8 @@ const tools = require('../util/tools/tools.js'); const crypto = require('crypto'); const token = ''; const printerUtil = require('../../api/main_endpoints/util/Printer.js'); +const User = require('../../api/main_endpoints/models/User.js'); +const { MEMBERSHIP_STATE } = require('../../api/util/constants'); let app = null; let test = null; @@ -37,6 +39,7 @@ chai.use(chaiHttp); describe('Printer', () => { before(done => { initializeTokenMock(); + tools.emptySchema(User); app = tools.initializeServer([ __dirname + '/../../api/main_endpoints/routes/Printer.js', @@ -132,8 +135,19 @@ describe('Printer', () => { it(`Should successfully process all ${TOTAL_CHUNKS} chunks sent (with valid token)`, async () => { let chunksProcessed = 0; - setTokenStatus(true); - + + const testUser = await new User({ + email: 'getuser@test.com', + password: 'Passw0rd', + firstName: 'Get', + lastName: 'User', + accessLevel: MEMBERSHIP_STATE.MEMBER, + emailVerified: true, + escrowPagesPrinted: 0 + }).save(); + + setTokenStatus(true, { _id: testUser._id }); + for (let i = 0; i < TOTAL_CHUNKS; i++) { let chunkStart = i * CHUNK_SIZE; let chunk = FAKE_PDF.slice(chunkStart, chunkStart + CHUNK_SIZE); @@ -145,6 +159,7 @@ describe('Printer', () => { .set('Authorization', `Bearer ${token}`) .type('form') .field('totalChunks', TOTAL_CHUNKS) + .field('totalPages', 1) .field('chunkIdx', i) .field('sides', 'one-sided') .field('copies', 1) @@ -157,6 +172,8 @@ describe('Printer', () => { } expect(chunksProcessed).to.equal(TOTAL_CHUNKS); + const userAfterPrinting = await User.findOne({ _id: testUser._id }); + expect(userAfterPrinting.escrowPagesPrinted).to.equal(1); }); }); }); From 897379e4f373ec7b14845482ce56a4cf5aefca0c Mon Sep 17 00:00:00 2001 From: evan Date: Sat, 25 Jul 2026 12:18:49 -0700 Subject: [PATCH 6/9] lint again --- test/api/Printer.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/api/Printer.js b/test/api/Printer.js index 33315e296..8910c0549 100644 --- a/test/api/Printer.js +++ b/test/api/Printer.js @@ -135,7 +135,7 @@ describe('Printer', () => { it(`Should successfully process all ${TOTAL_CHUNKS} chunks sent (with valid token)`, async () => { let chunksProcessed = 0; - + const testUser = await new User({ email: 'getuser@test.com', password: 'Passw0rd', @@ -147,7 +147,7 @@ describe('Printer', () => { }).save(); setTokenStatus(true, { _id: testUser._id }); - + for (let i = 0; i < TOTAL_CHUNKS; i++) { let chunkStart = i * CHUNK_SIZE; let chunk = FAKE_PDF.slice(chunkStart, chunkStart + CHUNK_SIZE); From c013555666d8322fc8b3bb8aa7a81a8e259c952a Mon Sep 17 00:00:00 2001 From: evan Date: Sat, 25 Jul 2026 12:23:37 -0700 Subject: [PATCH 7/9] save user before responding --- api/main_endpoints/routes/Printer.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/main_endpoints/routes/Printer.js b/api/main_endpoints/routes/Printer.js index 9c5a03315..fdb4b4d37 100644 --- a/api/main_endpoints/routes/Printer.js +++ b/api/main_endpoints/routes/Printer.js @@ -142,10 +142,10 @@ router.post('/sendPrintRequest', upload.single('chunk'), async (req, res) => { } await cleanUpChunks(dir, id); - res.status(OK).send(printId); - user.escrowPagesPrinted += Number(totalPages); await user.save(); + res.status(OK).send(printId); + } catch (err) { logger.error('/sendPrintRequest had an error: ', err); From 19ea1a02aa593817ac15a891f4ae32e3ea2c1c25 Mon Sep 17 00:00:00 2001 From: evan Date: Sat, 25 Jul 2026 12:34:53 -0700 Subject: [PATCH 8/9] !PRINTING.ENABLED && process.env.NODE_ENV !== 'test' --- api/main_endpoints/routes/Printer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/main_endpoints/routes/Printer.js b/api/main_endpoints/routes/Printer.js index fdb4b4d37..408dbcf1b 100644 --- a/api/main_endpoints/routes/Printer.js +++ b/api/main_endpoints/routes/Printer.js @@ -79,7 +79,7 @@ router.post('/sendPrintRequest', upload.single('chunk'), async (req, res) => { return res.sendStatus(decoded.status); } // this makes printing pass in unit tests, at some point need to test axios call - if (!PRINTING.ENABLED) { + if (!PRINTING.ENABLED && process.env.NODE_ENV !== 'test') { logger.warn('Printing is disabled, returning 200 and dummy print id to mock the printing server'); return res.status(OK).send({ printId: null }); } From 58e956edb41414b9249162f042a34ca002b8aba3 Mon Sep 17 00:00:00 2001 From: evan Date: Sat, 25 Jul 2026 22:24:42 -0700 Subject: [PATCH 9/9] delete unused files, remove promise from confirmation and unsubscribe --- api/cloud_api/email_templates/blastEmail.js | 16 ------- .../membershipConfirmationCode.js | 32 +++++++------ api/cloud_api/email_templates/test.js | 12 ----- .../email_templates/unsubscribeEmail.js | 20 ++++----- api/cloud_api/routes/Mailer.js | 45 +++++++++---------- 5 files changed, 45 insertions(+), 80 deletions(-) delete mode 100644 api/cloud_api/email_templates/blastEmail.js delete mode 100644 api/cloud_api/email_templates/test.js diff --git a/api/cloud_api/email_templates/blastEmail.js b/api/cloud_api/email_templates/blastEmail.js deleted file mode 100644 index ca0a56e88..000000000 --- a/api/cloud_api/email_templates/blastEmail.js +++ /dev/null @@ -1,16 +0,0 @@ -function blastEmail(user, recipient, subject, content) { - return new Promise((resolve, reject) => { - const sender = `"SCE SJSU 👻" <${user}>`; - return resolve({ - from: sender, - to: recipient, - subject: subject, - generateTextFromHTML: true, - html: ` - ${content} - `, - }); - }); -} - -module.exports = { blastEmail }; diff --git a/api/cloud_api/email_templates/membershipConfirmationCode.js b/api/cloud_api/email_templates/membershipConfirmationCode.js index 51dce5693..8deecdf39 100644 --- a/api/cloud_api/email_templates/membershipConfirmationCode.js +++ b/api/cloud_api/email_templates/membershipConfirmationCode.js @@ -1,20 +1,18 @@ function membershipConfirmationCode(user, recipient, confirmCode) { - return new Promise((resolve, reject) => { - return resolve({ - from: user, - to: recipient, - subject: `Your SCE Membership Code Is ${confirmCode}`, - generateTextFromHTML: true, - html: ` -

- Hi,
- Thank you for signing up for membership!
- Please use the below confirmation code when you - visit your profile page on the - SCE website to verify your membership:
${confirmCode} -

- ` - }); - }); + return { + from: user, + to: recipient, + subject: `Your SCE Membership Code Is ${confirmCode}`, + generateTextFromHTML: true, + html: ` +

+ Hi,
+ Thank you for signing up for membership!
+ Please use the below confirmation code when you + visit your profile page on the + SCE website to verify your membership:
${confirmCode} +

+ ` + }; } module.exports = { membershipConfirmationCode }; diff --git a/api/cloud_api/email_templates/test.js b/api/cloud_api/email_templates/test.js deleted file mode 100644 index 4ef862ca4..000000000 --- a/api/cloud_api/email_templates/test.js +++ /dev/null @@ -1,12 +0,0 @@ -module.exports = function(user, recipient, name) { - return { - from: user, - to: recipient, - subject: 'This is a test email', - generateTextFromHTML: true, - html: ` - Hi ${name},
-

This is a friendly test.

- ` - }; -}; diff --git a/api/cloud_api/email_templates/unsubscribeEmail.js b/api/cloud_api/email_templates/unsubscribeEmail.js index bf7615146..111905be2 100644 --- a/api/cloud_api/email_templates/unsubscribeEmail.js +++ b/api/cloud_api/email_templates/unsubscribeEmail.js @@ -1,15 +1,14 @@ function unsubscribeEmail(user, recipient, name) { - return new Promise((resolve, reject) => { - const url = + const url = process.env.VERIFICATION_BASE_URL || 'http://localhost:3000'; - const verifyLink = + const verifyLink = `${url}/emailPreferences?user=${recipient}`; - return resolve({ - from: user, - to: recipient, - subject: 'Unsubscribe from SCE Emails', - generateTextFromHTML: true, - html: ` + return { + from: user, + to: recipient, + subject: 'Unsubscribe from SCE Emails', + generateTextFromHTML: true, + html: ` Hi ${name || ''},

We appreciate you reducing email spam. To opt out of all our emails, click the link below @@ -18,8 +17,7 @@ function unsubscribeEmail(user, recipient, name) {

Thanks,

The Software and Computer Engineering Society

` - }); - }); + }; } module.exports = { unsubscribeEmail }; diff --git a/api/cloud_api/routes/Mailer.js b/api/cloud_api/routes/Mailer.js index 7481a3554..3e294bb31 100644 --- a/api/cloud_api/routes/Mailer.js +++ b/api/cloud_api/routes/Mailer.js @@ -88,7 +88,7 @@ router.post('/sendPasswordReset', async (req, res) => { } try { await apiHandler.sendEmail(template); - MetricsHandler.emailSent.inc({ type: 'verification' }); + MetricsHandler.emailSent.inc({ type: 'passwordReset' }); return res.sendStatus(OK); } catch(err) { logger.error('unable to generate password reset template:', err); @@ -112,12 +112,10 @@ router.post('/sendUnsubscribeEmail', async (req, res) => { const user = req.body.users[i]; try { let fullName = user.firstName + ' ' + user.lastName; - await unsubscribeEmail(USER, user.email, fullName) - .then((template) => { - apiHandler.sendEmail(template).then((_) => { - MetricsHandler.emailSent.inc({ type: 'unsubscribe' }); - }); - }); + const template = unsubscribeEmail(USER, user.email, fullName); + await apiHandler.sendEmail(template).then((_) => { + MetricsHandler.emailSent.inc({ type: 'unsubscribe' }); + }); } catch (error) { logger.error('unable to send unsubscribe email:', error); } @@ -145,23 +143,22 @@ router.post('/sendMembershipConfirmationCode', async (req, res) => { }); } - await membershipConfirmationCode(USER, recipientEmail, confirmationCode) - .then((template) => { - apiHandler - .sendEmail(template) - .then((_) => { - res.sendStatus(OK); - MetricsHandler.emailSent.inc({ type: 'membershipConfirmationCode' }); - }) - .catch((err) => { - logger.error('unable to send confirmation code: ', err); - res.sendStatus(SERVER_ERROR); - }); - }) - .catch((err) => { - logger.error('unable to generate member confirmation email template: ', err); - res.sendStatus(SERVER_ERROR); - }); + try { + const template = membershipConfirmationCode(USER, recipientEmail, confirmationCode); + apiHandler + .sendEmail(template) + .then((_) => { + res.sendStatus(OK); + MetricsHandler.emailSent.inc({ type: 'membershipConfirmationCode' }); + }) + .catch((err) => { + logger.error('unable to send confirmation code: ', err); + res.sendStatus(SERVER_ERROR); + }); + } catch(err) { + logger.error('unable to generate member confirmation email template: ', err); + res.sendStatus(SERVER_ERROR); + } }); module.exports = router;