Skip to content

Commit

Permalink
Changed emails templates and link from application validation expirat…
Browse files Browse the repository at this point in the history
…ion time

Co-authored-by: Daniel Ferreira <[email protected]>
  • Loading branch information
FranciscoCardoso913 and dsantosferreira committed Mar 11, 2023
1 parent 0bd0830 commit 709babb
Show file tree
Hide file tree
Showing 13 changed files with 82 additions and 83 deletions.
10 changes: 5 additions & 5 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -36,17 +36,17 @@ TEST_LOG_REQUESTS=false
ADMIN_EMAIL=[email protected]
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=[email protected]
MAIL_FROM=

# Password for email above
MAIL_FROM_PASSWORD=PgAnnVsAa6Sg8zJp6t
MAIL_FROM_PASSWORD=

# Cloudinary API URL to save images
CLOUDINARY_URL=
Expand Down
20 changes: 8 additions & 12 deletions src/api/middleware/application.js
Original file line number Diff line number Diff line change
@@ -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();
};
12 changes: 8 additions & 4 deletions src/api/middleware/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}
};
1 change: 0 additions & 1 deletion src/api/middleware/validators/validationReasons.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`
});
Expand Down
13 changes: 9 additions & 4 deletions src/api/routes/application.js
Original file line number Diff line number Diff line change
@@ -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();

Expand All @@ -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);
Expand All @@ -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);
}
Expand Down
1 change: 0 additions & 1 deletion src/email-templates/companyApplicationApproval.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,3 @@ export const REJECTION_NOTIFICATION = (companyName) => ({
template: "rejection_notification",
context: { companyName },
});

3 changes: 2 additions & 1 deletion src/email-templates/confirm-application.handlebars
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<h1>Confirm your NIJobs application</h1>
<p>Please follow this <a href="{{link}}" target="_blank">link</a> to finish the process. Note that the link will be expired in 5 minutes.</p>
<p>We have successfully received your application!</p>
<p>Please follow this <a href="{{link}}" target="_blank">link</a> to finish the process. Note that the link will expire in 10 minutes.</p>
<br>
<p>If you did not request this or need anything else, please contact us at <a href="mailto:[email protected]">[email protected]</a>!</p><br>
<p>Sincerely,</p>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
<h1>We have successfully received your application!</h1>
<p>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.</p>
<h1>Your application has been validated!</h1>
<p>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.</p>
<p>Your Application ID is {{applicationId}} and you registered {{companyName}}</p>
<p>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.</p>
<br>
<p>If you did not request this or if you need anything else, don't hesitate to contact us at <a href="mailto:[email protected]">[email protected]</a>!</p>
<br>
<p>Sincerely,</p>
<p>NIJobs team at NIAEFEUP</p>
<p>NIJobs team at NIAEFEUP</p>
19 changes: 4 additions & 15 deletions src/lib/emailService.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
});

Expand Down
15 changes: 1 addition & 14 deletions src/lib/token.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,9 @@
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 },
secret,
{ 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" });
18 changes: 10 additions & 8 deletions src/models/CompanyApplication.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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 });
};
Expand Down
2 changes: 1 addition & 1 deletion src/models/constants/ApplicationStatus.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ const ApplicationStatus = Object.freeze({

export default ApplicationStatus;

export const RECOVERY_LINK_EXPIRATION = 300;
export const VALIDATION_LINK_EXPIRATION = 600;
44 changes: 30 additions & 14 deletions src/services/application.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
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,
APPROVAL_NOTIFICATION,
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) {
Expand All @@ -34,6 +32,12 @@ export class CompanyApplicationEmailAlreadyInUse extends Error {
}
}

export class CompanyApplicationAlreadyValidated extends Error {
constructor(msg) {
super(msg);
}
}

class CompanyApplicationService {

async create({
Expand Down Expand Up @@ -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`;
}

Expand All @@ -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;

0 comments on commit 709babb

Please sign in to comment.