From 709babbeb278726e8fcba2e857898734c2be8ee3 Mon Sep 17 00:00:00 2001 From: Francisco Cardoso Date: Sat, 11 Feb 2023 14:36:07 +0000 Subject: [PATCH] Changed emails templates and link from application validation expiration time Co-authored-by: Daniel Ferreira --- .env | 10 ++--- src/api/middleware/application.js | 20 ++++----- src/api/middleware/auth.js | 12 +++-- .../validators/validationReasons.js | 1 - src/api/routes/application.js | 13 ++++-- .../companyApplicationApproval.js | 1 - .../confirm-application.handlebars | 3 +- ...new_company_application_company.handlebars | 7 +-- src/lib/emailService.js | 19 ++------ src/lib/token.js | 15 +------ src/models/CompanyApplication.js | 18 ++++---- src/models/constants/ApplicationStatus.js | 2 +- src/services/application.js | 44 +++++++++++++------ 13 files changed, 82 insertions(+), 83 deletions(-) diff --git a/.env b/.env index 2a00d51d..d4052b99 100644 --- a/.env +++ b/.env @@ -20,8 +20,8 @@ JWT_SECRET=Lendas contam que o Rui foi membro do IEEE. # Frontend Password Recovery Base Route PASSWORD_RECOVERY_LINK=https://localhost:3000/recover -# Froantend Application Confirmation Base Route -APPLICATION_CONFIRMATION_LINK=https://localhost:8087/apply/company/validate/ +# Frontend Application Confirmation Base Route +APPLICATION_CONFIRMATION_LINK=https://localhost:3000/apply/company/validate/ # Specifies the port in which the app will be exposed PORT=8087 @@ -36,17 +36,17 @@ TEST_LOG_REQUESTS=false ADMIN_EMAIL=ni@aefeup.pt ADMIN_PASSWORD=n1j0bs_ftw.12345 #CORS allowed origin - OVERRIDE IN PRODUCTION -ACCESS_CONTROL_ALLOW_ORIGIN= +ACCESS_CONTROL_ALLOW_ORIGIN=https://localhost # Mail service information. If you don't provide a MAIL_FROM, no emails will be sent. The app will execute no-ops and won't crash # However, if you want to send emails, you need to fill all of the following 2 fields # Check this for details on how to configure your personal account for testing (https://support.google.com/accounts/answer/185833?p=InvalidSecondFactor&visit_id=637446218993181653-2339409452&rd=1) # The email address from which the emails are sent -MAIL_FROM=jacky88@ethereal.email +MAIL_FROM= # Password for email above -MAIL_FROM_PASSWORD=PgAnnVsAa6Sg8zJp6t +MAIL_FROM_PASSWORD= # Cloudinary API URL to save images CLOUDINARY_URL= diff --git a/src/api/middleware/application.js b/src/api/middleware/application.js index 904ef3b7..66c181f7 100644 --- a/src/api/middleware/application.js +++ b/src/api/middleware/application.js @@ -1,17 +1,13 @@ import CompanyApplication, { CompanyApplicationRules } from "../../models/CompanyApplication.js"; import { APIError, ErrorTypes } from "./errorHandler.js"; import { StatusCodes as HTTPStatus } from "http-status-codes/build/cjs/status-codes.js"; -export const exceededCreationTimeLimit = async (email) => { - const cursor = await CompanyApplication.findOne({ email, isVerified: false }).exec(); - if (cursor !== null && Date.now() - cursor.submittedAt < 5000 * 60) { - throw new APIError(HTTPStatus.FORBIDDEN, ErrorTypes.FORBIDDEN, CompanyApplicationRules.APPLICATION_RECENTLY_CREATED); - } - return true; -}; +import { VALIDATION_LINK_EXPIRATION } from "../../models/constants/ApplicationStatus.js"; +import { SECOND_IN_MS } from "../../models/constants/TimeConstants.js"; -export const deleteApplications = async (email) => { - await CompanyApplication.deleteMany({ email: email, isVerified: false }).catch(function(error) { - console.error(error); - throw (error); // Failure - }); +export const exceededCreationTimeLimit = async (req, res, next) => { + const application = await CompanyApplication.findOne({ email: req.body.email, isVerified: false }); + if (application !== null && Date.now() < application.submittedAt.getTime() + (VALIDATION_LINK_EXPIRATION * SECOND_IN_MS)) { + return next(new APIError(HTTPStatus.FORBIDDEN, ErrorTypes.FORBIDDEN, CompanyApplicationRules.APPLICATION_RECENTLY_CREATED.msg)); + } + return next(); }; diff --git a/src/api/middleware/auth.js b/src/api/middleware/auth.js index 48c5539c..78bba6dc 100644 --- a/src/api/middleware/auth.js +++ b/src/api/middleware/auth.js @@ -79,15 +79,19 @@ export const hasAdminPrivileges = async (req, res, next) => { export const validToken = (req, res, next) => { try { - const decoded = verifyAndDecodeToken(req.params.token, config.jwt_secret, next); + const decoded = verifyAndDecodeToken(req.params.token, config.jwt_secret); storeInLocals(req, { token: decoded, }); return next(); - } catch (err) { - console.log(err); - return next(err); + } catch (jwtErr) { + console.log(jwtErr); + if (jwtErr.name === "TokenExpiredError") { + return next(new APIError(HTTPStatus.FORBIDDEN, ErrorTypes.FORBIDDEN, ValidationReasons.EXPIRED_TOKEN)); + } else { + return next(new APIError(HTTPStatus.FORBIDDEN, ErrorTypes.FORBIDDEN, ValidationReasons.INVALID_TOKEN)); + } } }; diff --git a/src/api/middleware/validators/validationReasons.js b/src/api/middleware/validators/validationReasons.js index b32e0369..389621f6 100644 --- a/src/api/middleware/validators/validationReasons.js +++ b/src/api/middleware/validators/validationReasons.js @@ -49,7 +49,6 @@ const ValidationReasons = Object.freeze({ IMAGE_FORMAT: "formats-supported-png-jpeg-jpg", OFFER_BLOCKED_ADMIN: "offer-blocked-by-admin", OFFER_HIDDEN: "offer-is-hidden", - ALREADY_VALIDATED: "application-already-validated", NON_EXISTING_APPLICATION: "application-does-not-exist", FILE_TOO_LARGE: (max) => `file-cant-be-larger-than-${max}MB` }); diff --git a/src/api/routes/application.js b/src/api/routes/application.js index a0318561..2c22a98b 100644 --- a/src/api/routes/application.js +++ b/src/api/routes/application.js @@ -1,9 +1,10 @@ import { Router } from "express"; import * as validators from "../middleware/validators/application.js"; -import ApplicationService from "../../services/application.js"; +import ApplicationService, { CompanyApplicationAlreadyValidated } from "../../services/application.js"; import * as applicationMiddleware from "../middleware/application.js"; import { validToken } from "../middleware/auth.js"; import { StatusCodes as HTTPStatus } from "http-status-codes/build/cjs/status-codes.js"; +import { buildErrorResponse, ErrorTypes } from "../middleware/errorHandler.js"; const router = Router(); @@ -13,11 +14,10 @@ export default (app) => { /** * Creates a new Company Application */ - router.post("/", validators.create, async (req, res, next) => { + router.post("/", validators.create, applicationMiddleware.exceededCreationTimeLimit, async (req, res, next) => { try { - await applicationMiddleware.exceededCreationTimeLimit(req.body.email); - await applicationMiddleware.deleteApplications(req.body.email); const applicationService = new ApplicationService(); + await applicationService.deleteApplications(req.body.email); // This is safe since the service is destructuring the passed object and the fields have been validated const application = await applicationService.create(req.body); return res.json(application); @@ -32,6 +32,11 @@ export default (app) => { await new ApplicationService().applicationValidation(id); return res.status(HTTPStatus.OK).json({}); } catch (err) { + if (err instanceof CompanyApplicationAlreadyValidated) { + return res + .status(HTTPStatus.CONFLICT) + .json(buildErrorResponse(ErrorTypes.FORBIDDEN, [{ msg: err.message }])); + } console.error(err); return next(err); } diff --git a/src/email-templates/companyApplicationApproval.js b/src/email-templates/companyApplicationApproval.js index a4423b35..078677d4 100644 --- a/src/email-templates/companyApplicationApproval.js +++ b/src/email-templates/companyApplicationApproval.js @@ -23,4 +23,3 @@ export const REJECTION_NOTIFICATION = (companyName) => ({ template: "rejection_notification", context: { companyName }, }); - diff --git a/src/email-templates/confirm-application.handlebars b/src/email-templates/confirm-application.handlebars index aceaa777..3924bd9f 100644 --- a/src/email-templates/confirm-application.handlebars +++ b/src/email-templates/confirm-application.handlebars @@ -1,5 +1,6 @@

Confirm your NIJobs application

-

Please follow this link to finish the process. Note that the link will be expired in 5 minutes.

+

We have successfully received your application!

+

Please follow this link to finish the process. Note that the link will expire in 10 minutes.


If you did not request this or need anything else, please contact us at nijobs@aefeup.pt!


Sincerely,

diff --git a/src/email-templates/new_company_application_company.handlebars b/src/email-templates/new_company_application_company.handlebars index 8966ef10..4c03b7b8 100644 --- a/src/email-templates/new_company_application_company.handlebars +++ b/src/email-templates/new_company_application_company.handlebars @@ -1,8 +1,9 @@ -

We have successfully received your application!

-

We will now review your application, and in case you're approved, you will receive another email with further instructions in order to complete your registration.

+

Your application has been validated!

+

We will now review your application, and in case you're approved, you will receive another email with further instructions in order to complete your registration.

Your Application ID is {{applicationId}} and you registered {{companyName}}

+

Once you're approved, you will receive an email, and then you can log into NIJobs! Do not forget your password, you will need it on the first login.


If you did not request this or if you need anything else, don't hesitate to contact us at nijobs@aefeup.pt!


Sincerely,

-

NIJobs team at NIAEFEUP

\ No newline at end of file +

NIJobs team at NIAEFEUP

diff --git a/src/lib/emailService.js b/src/lib/emailService.js index 2043cede..d046f7ea 100644 --- a/src/lib/emailService.js +++ b/src/lib/emailService.js @@ -6,24 +6,13 @@ export class EmailService { async init({ email: user, password: pass }) { this.email = user; - /* const transporter = await nodemailer.createTransport({ - pool: true, - host: "smtp.gmail.com", - port: 465, - secure: true, - auth: { - user, - pass - }, - connectionTimeout: 30000 - }); - console.log("transporter");*/ const transporter = nodemailer.createTransport({ - host: "smtp.ethereal.email", + host: 'smtp.ethereal.email', port: 587, + secure: false, // true for 465, false for other ports auth: { - user: "naomie.koelpin2@ethereal.email", - pass: "NGEVbMnTZzyA3MQD3V" + user: "magdalen.labadie@ethereal.email", // generated ethereal user + pass: "GjdJdafFyj5svQBzJJ"// generated ethereal password } }); diff --git a/src/lib/token.js b/src/lib/token.js index 5728271a..be8ed801 100644 --- a/src/lib/token.js +++ b/src/lib/token.js @@ -1,7 +1,4 @@ import jwt from "jsonwebtoken"; -import { APIError, ErrorTypes } from "../api/middleware/errorHandler.js"; -import { StatusCodes as HTTPStatus } from "http-status-codes/build/cjs/status-codes.js"; -import ValidationReasons from "../api/middleware/validators/validationReasons.js"; export const generateToken = (data, secret, expiresInSeconds) => jwt.sign( { ...data }, @@ -9,14 +6,4 @@ export const generateToken = (data, secret, expiresInSeconds) => jwt.sign( { expiresIn: `${expiresInSeconds} seconds`, algorithm: "HS256" } ); -export const verifyAndDecodeToken = (token, secret, next) => { - try { - return jwt.verify(token, secret, { algorithm: "HS256" }); - } catch (jwtErr) { - if (jwtErr.name === "TokenExpiredError") { - return next(new APIError(HTTPStatus.FORBIDDEN, ErrorTypes.FORBIDDEN, ValidationReasons.EXPIRED_TOKEN)); - } else { - return next(new APIError(HTTPStatus.FORBIDDEN, ErrorTypes.FORBIDDEN, ValidationReasons.INVALID_TOKEN)); - } - } -}; +export const verifyAndDecodeToken = (token, secret) => jwt.verify(token, secret, { algorithm: "HS256" }); diff --git a/src/models/CompanyApplication.js b/src/models/CompanyApplication.js index c51b4b47..e3887313 100644 --- a/src/models/CompanyApplication.js +++ b/src/models/CompanyApplication.js @@ -2,9 +2,7 @@ import mongoose from "mongoose"; import ApplicationStatus from "./constants/ApplicationStatus.js"; import CompanyApplicationConstants from "./constants/CompanyApplication.js"; import { checkDuplicatedEmail } from "../api/middleware/validators/validatorUtils.js"; -import { APIError, ErrorTypes } from "../api/middleware/errorHandler.js"; -import { StatusCodes as HTTPStatus } from "http-status-codes/build/cjs/status-codes.js"; -import ValidationReasons from "../api/middleware/validators/validationReasons.js"; + const { Schema } = mongoose; @@ -37,8 +35,11 @@ export const CompanyApplicationRules = Object.freeze({ msg: "company-application-already-reviewed", }, APPLICATION_RECENTLY_CREATED: { - msg: "company-application-already-created-less-than-5-minutes-ago", + msg: "company-application-recently-created", }, + APPLICATION_ALREADY_VALIDATED: { + msg: "application-already-validated", + } }); export const CompanyApplicationProps = { @@ -127,9 +128,8 @@ function validateMutuallyExclusiveEvents(field) { export const applicationUniqueness = async (email) => { const existingApplications = await CompanyApplication.find({ email }); if (existingApplications.some((application) => - (application.state === ApplicationStatus.PENDING || - application.state === ApplicationStatus.APPROVED) && - application.isVerified) + application.state === ApplicationStatus.PENDING || + application.state === ApplicationStatus.APPROVED) ) { throw new Error(CompanyApplicationRules.ONLY_ONE_APPLICATION_ACTIVE_PER_EMAIL.msg); } @@ -164,7 +164,9 @@ export const isRejectable = (application) => { CompanyApplicationSchema.methods.companyValidation = function() { - if (this.isVerified) throw new APIError(HTTPStatus.FORBIDDEN, ErrorTypes.FORBIDDEN, ValidationReasons.ALREADY_VALIDATED); + if (this.isVerified) + throw new Error(CompanyApplicationRules.APPLICATION_ALREADY_VALIDATED.msg); + this.isVerified = true; return this.save({ validateModifiedOnly: true }); }; diff --git a/src/models/constants/ApplicationStatus.js b/src/models/constants/ApplicationStatus.js index e283fff9..72f35baf 100644 --- a/src/models/constants/ApplicationStatus.js +++ b/src/models/constants/ApplicationStatus.js @@ -7,4 +7,4 @@ const ApplicationStatus = Object.freeze({ export default ApplicationStatus; -export const RECOVERY_LINK_EXPIRATION = 300; +export const VALIDATION_LINK_EXPIRATION = 600; diff --git a/src/services/application.js b/src/services/application.js index 87feed79..9ea3f821 100644 --- a/src/services/application.js +++ b/src/services/application.js @@ -1,11 +1,10 @@ import CompanyApplication, { CompanyApplicationRules } from "../models/CompanyApplication.js"; import { generateToken } from "../lib/token.js"; import hash from "../lib/passwordHashing.js"; -import { RECOVERY_LINK_EXPIRATION } from "../models/constants/ApplicationStatus.js"; +import { VALIDATION_LINK_EXPIRATION } from "../models/constants/ApplicationStatus.js"; import { APPLICATION_CONFIRMATION } from "../email-templates/companyApplicationConfirmation.js"; import AccountService from "./account.js"; import EmailService from "../lib/emailService.js"; -import { StatusCodes as HTTPStatus } from "http-status-codes/build/cjs/status-codes.js"; import { NEW_COMPANY_APPLICATION_ADMINS, NEW_COMPANY_APPLICATION_COMPANY, @@ -13,8 +12,7 @@ import { REJECTION_NOTIFICATION, } from "../email-templates/companyApplicationApproval.js"; import config from "../config/env.js"; -import { APIError, ErrorTypes } from "../api/middleware/errorHandler.js"; -import ValidationReasons from "../api/middleware/validators/validationReasons.js"; + export class CompanyApplicationNotFound extends Error { constructor(msg) { @@ -34,6 +32,12 @@ export class CompanyApplicationEmailAlreadyInUse extends Error { } } +export class CompanyApplicationAlreadyValidated extends Error { + constructor(msg) { + super(msg); + } +} + class CompanyApplicationService { async create({ @@ -219,7 +223,7 @@ class CompanyApplicationService { } buildConfirmationLink(id) { - const token = generateToken({ _id: id }, config.jwt_secret, RECOVERY_LINK_EXPIRATION); + const token = generateToken({ _id: id }, config.jwt_secret, VALIDATION_LINK_EXPIRATION); return `${config.application_confirmation_link}/${token}/confirm`; } @@ -234,19 +238,31 @@ class CompanyApplicationService { throw err; } } + async applicationValidation(id) { const application = await this.findById(id); - application.companyValidation(); - await EmailService.sendMail({ - to: config.mail_from, - ...NEW_COMPANY_APPLICATION_ADMINS(application.email, application.companyName, application.motivation) - }); - await EmailService.sendMail({ - to: application.email, - ...NEW_COMPANY_APPLICATION_COMPANY(application.companyName, application._id.toString()) - }); + try { + application.companyValidation(); + await EmailService.sendMail({ + to: config.mail_from, + ...NEW_COMPANY_APPLICATION_ADMINS(application.email, application.companyName, application.motivation) + }); + + await EmailService.sendMail({ + to: application.email, + ...NEW_COMPANY_APPLICATION_COMPANY(application.companyName, application._id.toString()) + }); + } catch (err) { + console.error(err); + throw new CompanyApplicationAlreadyValidated(CompanyApplicationRules.APPLICATION_ALREADY_VALIDATED.msg); + } + } + + async deleteApplications(email) { + await CompanyApplication.deleteMany({ email: email, isVerified: false }); } } + export default CompanyApplicationService;