Skip to content

Commit

Permalink
Merge pull request #15 from waterloop/applicant-email-reply
Browse files Browse the repository at this point in the history
Applicant email reply
  • Loading branch information
Jeff Ma authored Dec 11, 2022
2 parents 92fc969 + f205791 commit f8565f4
Show file tree
Hide file tree
Showing 61 changed files with 3,668 additions and 3,963 deletions.
3 changes: 0 additions & 3 deletions .babelrc

This file was deleted.

9 changes: 9 additions & 0 deletions .babelrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const rootImportOpts = {
root: __dirname,
rootPathSuffix: 'src',
};

module.exports = {
presets: ['@babel/preset-env'],
plugins: [['babel-plugin-root-import', rootImportOpts]],
};
8 changes: 5 additions & 3 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@
"no-console": "warn", // Warning
"no-duplicate-case": "error", // Error
"no-extra-semi": "off", // NO error
"eqeqeq": "error", // Error
"default-case": "error", // Error
"eqeqeq": "error",
"default-case": "error",
"comma-dangle": ["error", "always-multiline"],
"no-shadow": "off",
"no-plusplus": "error",
Expand All @@ -31,6 +31,8 @@
"react/jsx-filename-extension": ["warn", { "extensions": [".js", ".jsx"] }],
"react/jsx-props-no-spreading": "off",
"react/prop-types": "off",
"import/no-anonymous-default-export": "off"
"import/no-anonymous-default-export": "off",
"spaced-comment": "off",
"no-bitwise": "off"
}
}
6 changes: 6 additions & 0 deletions jsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"compilerOptions": {
"baseUrl": "src"
},
"include": ["src/frontend"]
}
15 changes: 8 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
"dependencies": {
"@babel/cli": "^7.16.0",
"@date-io/date-fns": "1.x",
"@emotion/react": "^11.8.1",
"@material-ui/core": "^4.12.3",
"@emotion/react": "^11.10.4",
"@emotion/styled": "^11.10.4",
"@mui/icons-material": "^5.0.5",
"@mui/lab": "^5.0.0-alpha.74",
"@mui/material": "^5.3.0",
Expand All @@ -27,10 +27,10 @@
"js-cookie": "^3.0.1",
"knex": "^2.1.0",
"moment": "^2.29.4",
"nodemailer": "^6.8.0",
"nodemon": "^2.0.16",
"pg": "^8.7.3",
"pg-connection-string": "^2.5.0",
"postgres": "^3.1.0",
"ramda": "^0.28.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
Expand All @@ -41,7 +41,6 @@
"react-router-dom": "^5.3.0",
"react-scripts": "4.0.3",
"redux": "^4.1.1",
"redux-devtools-extension": "^2.13.9",
"styled-components": "^5.3.1",
"typeface-ibm-plex-sans": "^1.1.13"
},
Expand All @@ -55,8 +54,8 @@
"build-storybook": "build-storybook -s public",
"dev": "yarn migrate && yarn seed && cross-env NODE_ENV=development nodemon --exec ./node_modules/.bin/babel-node src/backend/index.js",
"test": "echo \"Error: no test specified\" && exit 1",
"test:integration": "cross-env NODE_ENV=test cross-env PORT=9001 mocha --exit -r esm ./src/backend/tests/integration/*",
"test:integration:watch": "cross-env NODE_ENV=test cross-env PORT=9001 mocha -r esm --watch ./src/backend/tests/integration/*",
"test:integration": "cross-env NODE_ENV=test cross-env PORT=9001 mocha -r @babel/register --exit ./src/backend/tests/integration/*",
"test:integration:watch": "cross-env NODE_ENV=test cross-env PORT=9001 mocha -r @babel/register --watch ./src/backend/tests/integration/*",
"docker:test": "docker-compose run --service-ports test-db ",
"docker:dev": "docker-compose run --service-ports dev-db ",
"migrate": "knex migrate:latest --knexfile src/backend/knexfile.js",
Expand Down Expand Up @@ -87,6 +86,7 @@
]
},
"devDependencies": {
"babel-plugin-root-import": "^6.6.0",
"@babel/core": "^7.16.0",
"@babel/node": "^7.16.0",
"@babel/preset-env": "^7.16.4",
Expand All @@ -113,7 +113,8 @@
"lint-staged": "^11.2.3",
"mocha": "^8.4.0",
"prettier": "^2.4.1",
"react-error-overlay": "6.0.9"
"react-error-overlay": "6.0.9",
"redux-devtools-extension": "^2.13.9"
},
"resolutions": {
"babel-loader": "8.1.0"
Expand Down
20 changes: 7 additions & 13 deletions src/backend/api/applications/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,11 @@ import getApplications from './get-application';
import getApplicationByEmail from './get-application-by-email';
import getApplicationStatuses from '../application-status/get-application-statuses';
import updateApplicationStatus from './update-application-status';
import validationCheck from '../../utils/validation-check';
import validationCheck from '~/backend/utils/validation-check';
import { body, param, query } from 'express-validator';
import { validateRequest } from '~/backend/google-auth';

// TODO: Move to a constants.js file if multiple references to this are needed.
// Alternatively perform the status check on db side and get rid of this constant.
const STATUSES = [
'app_pending',
'app_reject',
'app_undecided',
'interview_pending',
'interview_reject',
'interview_undecided',
'final_accept',
];
import { STATUSES } from '~/backend/utils/constants';

const router = express.Router();

Expand All @@ -29,19 +20,21 @@ router.get(
.matches(/^(FALL|WINTER|SPRING)-2\d{3}$/),
],
validationCheck,
validateRequest,
getApplications,
);

router.get(
'/applicant/:email',
[param('email').isEmail()],
validationCheck,
validateRequest,
getApplicationByEmail,
);

router.get('/statuses', getApplicationStatuses);

// NOTE: refer to express-validator for documentation.
// Route is used by main site, so we don't need a validateRequest middleware.
router.post(
'/',
[
Expand Down Expand Up @@ -70,6 +63,7 @@ router.patch(
'/applicant/status',
[body('id').isInt(), body('status').isString().isIn(STATUSES)],
validationCheck,
validateRequest,
updateApplicationStatus,
);

Expand Down
2 changes: 2 additions & 0 deletions src/backend/api/configuration/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import getConfiguration from './get-configuration';
import changeConfiguration from './update-configuration';
import validationCheck from '../../utils/validation-check';
import { body } from 'express-validator';
import { validateRequest } from '../../google-auth';

const router = express.Router();

Expand All @@ -20,6 +21,7 @@ router.patch(
body('*.value', 'expected value to be string').isString().notEmpty(),
],
validationCheck,
validateRequest,
changeConfiguration,
);

Expand Down
48 changes: 48 additions & 0 deletions src/backend/api/email/helper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
const { google } = require('googleapis');
const MailComposer = require('nodemailer/lib/mail-composer');

const encodeMessage = (message) =>
Buffer.from(message)
.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');

const createMail = async (options) => {
const mailComposer = new MailComposer(options);
const message = await mailComposer.compile().build();
return encodeMessage(message);
};

export const sendEmail = async (message, token) => {
try {
const oAuth2Client = new google.auth.OAuth2(process.env.GOOGLE_CLIENT_ID);
oAuth2Client.setCredentials({
access_token: token,
scope: 'https://www.googleapis.com/auth/gmail.send',
});
const gmail = google.gmail({ version: 'v1', auth: oAuth2Client });
const options = {
to: message.to,
subject: message.subject,
html: message.body,
textEncoding: 'base64',
headers: [
{ key: 'X-Application-Developer', value: 'Gordon Wang' },
{ key: 'X-Application-Version', value: 'v1.0.0.2' },
],
};

const rawMessage = await createMail(options);
const { data: { id } = {} } = await gmail.users.messages.send({
userId: 'me',
requestBody: {
raw: rawMessage,
},
});
return id;
} catch (err) {
console.log('sendMail error', err);
return -1;
}
};
11 changes: 9 additions & 2 deletions src/backend/api/email/index.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import express from 'express';

import updateEmailSent from './update-email-sent';
import updateEmailStatus from './update-email-sent';
import validationCheck from '../../utils/validation-check';
import { validateRequest } from '../../google-auth';
import { body } from 'express-validator';

const router = express.Router();

// NOTE: status checks will be done on DB end.
router.patch('/', [body('id').isInt()], validationCheck, updateEmailSent);
router.patch(
'/',
[body('id').isInt()],
validationCheck,
validateRequest,
updateEmailStatus,
);

export default router;
65 changes: 23 additions & 42 deletions src/backend/api/email/update-email-sent.js
Original file line number Diff line number Diff line change
@@ -1,45 +1,26 @@
import db from '../../db';
import db from '~/backend/db';
import { sendEmail } from './helper';

export default (req, res) => {
export default async (req, res) => {
const appID = req.body.id;
// Try applications db first:
db.applications
.updateEmailSent(appID)
.then((response) => {
if (Array.isArray(response) && response.length !== 0) {
res.send(response[0]);
} else {
// If no entries matched criteria in db, then attempt to update interview table email_sent column
db.interviews.updateEmailSent(appID).then((resp2) => {
if (typeof resp2 === 'number') {
let errMsg;
switch (resp2) {
// TODO: We should have an error struct in the future to distinguish between these 2 errors more easily.
case -1:
errMsg = `Could not find application (and corresponding interview entry) with ID ${appID}.`;
break;
case -2:
errMsg = `Cannot modify application/interview entry with ID ${appID}: invalid status.`;
break;
default:
break;
}
console.error(errMsg);
res.status(403).send(errMsg);
} else if (Array.isArray(resp2) && resp2.length === 1) {
res.send(resp2[0]);
} else {
// We should never have more than 1 interview entry:
throw new Error(
'Multiple interview entries detected for single application! Please sanitize the database.',
);
}
});
}
})
.catch((err) => {
const errMsg = `Could not modify email-sent status: ${err}`;
console.error(errMsg);
res.status(500).send(errMsg);
});

// send the actual email, then check if email sent correctly
if (req.body.accessToken) {
await sendEmail(req.body, req.body.accessToken); // TODO: check if accessToken sent securely.
}

try {
// if email sent correctly, proceed with code below, else abort procedure.
const response = await db.applications.updateEmailStatus(appID);
if (Array.isArray(response) && response.length !== 0) {
res.send({ ...response[0] });
} else {
// invalid ID or cannot send email for application with the status:
res.sendStatus(403);
}
} catch (err) {
const errMsg = `Could not modify email-sent status: ${err}`;
console.error(errMsg);
res.status(500).send(errMsg);
}
};
5 changes: 4 additions & 1 deletion src/backend/api/interviews/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import express from 'express';
import updateOrAddInterview from './update-or-add-interview';
import getInterviewsByTerm from './get-interview';
import getInterviewById from './get-interview-by-application-id';
import validationCheck from '../../utils/validation-check';
import validationCheck from '~/backend/utils/validation-check';
import { body, param, query } from 'express-validator';
import { validateRequest } from '~/backend/google-auth';

const router = express.Router();

Expand All @@ -15,6 +16,7 @@ router.get(
.matches(/^(FALL|WINTER|SPRING)-2\d{3}$/),
],
validationCheck,
validateRequest,
getInterviewsByTerm,
);

Expand All @@ -27,6 +29,7 @@ router.post(
body('application_id').isInt(),
],
validationCheck,
validateRequest,
updateOrAddInterview,
);

Expand Down
Loading

0 comments on commit f8565f4

Please sign in to comment.