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}
-
+ 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}
+
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 { + 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 + ` + }; } module.exports = { passwordReset }; 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/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..3e294bb31 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,15 +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); - } - const scopes = ['https://mail.google.com/']; - const pathToToken = __dirname + '/../../config/token.json'; +const scopes = ['https://mail.google.com/']; +const pathToToken = __dirname + '/../../config/token.json'; + +async function maybeRefreshTokenFromGcp() { const apiHandler = new SceGoogleApiHandler(scopes, pathToToken); const tokenJson = await apiHandler.checkIfTokenFileExists(); @@ -40,25 +36,35 @@ 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 maybeRefreshTokenFromGcp(); - 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); - }); + 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:', e); + return res.sendStatus(BAD_REQUEST); + } + + try { + await apiHandler.sendEmail(template); + MetricsHandler.emailSent.inc({ type: 'verification' }); + return res.sendStatus(OK); + } catch(e) { + logger.error('unable to send verification email:', e); + res.sendStatus(BAD_REQUEST); + } }); // Routing post /sendPasswordReset calls the sendEmail function @@ -67,39 +73,27 @@ 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(); + 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); + } + try { + await apiHandler.sendEmail(template); + MetricsHandler.emailSent.inc({ type: 'passwordReset' }); + 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 @@ -111,9 +105,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) { @@ -121,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); } @@ -140,21 +129,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(); - 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(); - } + await maybeRefreshTokenFromGcp(); + + const apiHandler = new SceGoogleApiHandler(scopes, pathToToken); const { recipientEmail, confirmationCode } = req.body; @@ -165,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; 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/api/main_endpoints/routes/Printer.js b/api/main_endpoints/routes/Printer.js index 9cd1daae9..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 }); } @@ -124,23 +124,28 @@ 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"'; - // { print_id: null | string } - const printId = printRes.data; + if (process.env.NODE_ENV !== 'test') { - await cleanUpChunks(dir, id); - res.status(OK).send(printId); + // 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); user.escrowPagesPrinted += Number(totalPages); await user.save(); + res.status(OK).send(printId); + } catch (err) { logger.error('/sendPrintRequest had an error: ', err); 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)}/> -{ 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); diff --git a/test/api/Printer.js b/test/api/Printer.js index 7e285dc1a..8910c0549 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,7 +135,18 @@ 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; @@ -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); }); }); });