Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Features/email confirmation #51

Open
wants to merge 3 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ services:
- NODE_ENV=dev
- SECRET=Ks10daK3g
- WISP_PROBLEMS_URL=http://problems-api:3000
- SENDGRID_API_KEY=${SENDGRID_API_KEY}
working_dir: /app
command: npm run start:local
ports:
Expand Down
42 changes: 42 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"@babel/core": "^7.0.0",
"@babel/preset-typescript": "^7.3.3",
"@istanbuljs/nyc-config-typescript": "^0.1.3",
"@sendgrid/mail": "^7.1.1",
"@types/bcryptjs": "^2.4.2",
"@types/chai": "^4.1.7",
"@types/chai-as-promised": "^7.1.0",
Expand Down
12 changes: 11 additions & 1 deletion src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { requestLoggerConfig } from "./config/requestLogger";
import { errorLoggerConfig } from "./config/errorLogger";
import { userRouter } from "./routes/user";
import { authRouter } from "./routes/auth";
import { logger } from "./config/logger";
import mailer from "@sendgrid/mail";

export const port: Number = parseInt(process.env.SERVER_PORT) || 3000;
const app: Application = express();
Expand All @@ -24,11 +26,19 @@ app.use("/auth", authRouter);

const env = process.env.NODE_ENV || "dev";

const emailAPIKey: string = process.env.SENDGRID_API_KEY || null;
if (!emailAPIKey) {
logger.error("Sendgrid API Key for mailer service not set (SENDGRID_API_KEY)");
} else {
mailer.setApiKey(emailAPIKey);
}


app.use((req: Request, res: Response) => {
res.status(404).send({
status: 404,
message: "Invalid route"
});
});

export { app };
export { app, mailer };
4 changes: 3 additions & 1 deletion src/controllers/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ const authController = {
if (!user) {
res.status(statusCodes.BAD_REQUEST).send({ status: statusCodes.BAD_REQUEST, message: "Invalid email or password" });
} else {
if (!bcryptPassword.validate(password, user.password)) {
if (!user.confirmation.isConfirmed) {
res.status(statusCodes.UNAUTHORIZED).send({ status: statusCodes.UNAUTHORIZED, message: "Please confirm your email address before logging in (check the email linked to your account)." })
} else if (!bcryptPassword.validate(password, user.password)) {
res.status(statusCodes.BAD_REQUEST).send({ status: statusCodes.BAD_REQUEST, message: "Invalid email or password" });
} else {
if (user.platformData.codeforces.username) await codeforces.updateUserProblems(user);
Expand Down
30 changes: 30 additions & 0 deletions src/controllers/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { errorMessage } from "../config/errorFormatter";
import { bcryptPassword } from "../config/bcrypt";
import { statusCodes } from "../config/statusCodes";
import { codeforces } from "../util/codeforces";
import { sendConfirmationEmail } from "../util/emailConfirmation";

const userController = {

Expand Down Expand Up @@ -52,9 +53,14 @@ const userController = {
else {
const userData: IUser = {
...req.body,
confirmation: {
isConfirmed: false,
confirmationCode: bcryptPassword.generateHash(Math.random().toString())
},
password: bcryptPassword.generateHash(req.body.password)
};
let newUser: IUserModel = await userDBInteractions.create(new User(userData));
await sendConfirmationEmail(newUser);
newUser = newUser.toJSON();
delete newUser.password;
res.status(statusCodes.SUCCESS).send(newUser);
Expand All @@ -65,6 +71,30 @@ const userController = {
}
},

confirmEmail: async (req: Request, res: Response) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
res.status(statusCodes.MISSING_PARAMS).json(errors.formatWith(errorMessage).array()[0]);
} else {
try {
const { userId, confirmationCode } = req.params;
const user: IUserModel = await userDBInteractions.find(userId);
if (!user)
res.status(statusCodes.NOT_FOUND).send({ status: statusCodes.NOT_FOUND, message: "User not found" });
else if (user.confirmation.isConfirmed) {
res.status(statusCodes.SUCCESS).send({ status: statusCodes.SUCCESS, message: "Your account is already confirmed! You have access to WISP" });
} else if (user.confirmation.confirmationCode === confirmationCode) {
user.save()
res.status(statusCodes.SUCCESS).send({ status: statusCodes.SUCCESS, message: "Your account is now confirmed! You are now able to log into https://wisp.training" })
} else {
res.status(statusCodes.BAD_REQUEST).send({ status: statusCodes.BAD_REQUEST, message: "Invalid confirmation code" })
}
} catch (error) {
res.status(statusCodes.SERVER_ERROR).send(error);
}
}
},

update: async (req: Request, res: Response) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
Expand Down
4 changes: 4 additions & 0 deletions src/database/models/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ const userSchema: Schema = new Schema({
},
},
},
confirmation: {
isConfirmed: Boolean,
confirmationCode: String
}
}, {
timestamps: true,
});
Expand Down
4 changes: 4 additions & 0 deletions src/interfaces/IUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ export interface IUser {
lastSubmission: IProblem;
};
};
confirmation: {
isConfirmed: boolean;
confirmationCode: string;
}
}

export interface IInfo {
Expand Down
2 changes: 2 additions & 0 deletions src/routes/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ userRouter.get("/", userValidator("GET /users"), userController.index);

userRouter.get("/:userId", userValidator("GET /users/:userId"), userController.show);

userRouter.get("/:userId/confirmEmail/:confirmationCode", userValidator("GET /users/:userId/confirmEmail/:confirmationCode"), userController.confirmEmail);

userRouter.post("/", userValidator("POST /users"), userController.create);

userRouter.put("/:userId", userValidator("PUT /users/:userId"), userController.update);
Expand Down
27 changes: 27 additions & 0 deletions src/util/emailConfirmation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { mailer } from "../app";
import { MailDataRequired } from "@sendgrid/mail";
import { logger } from "../config/logger";
import { IUserModel } from "database/models/user";


export async function sendConfirmationEmail(user: IUserModel) {
const emailToSend: MailDataRequired = {
to: user.email,
from: '[email protected]',
subject: 'Confirm your email to activate your WISP account',
html: `
<h1>Welcome to WISP</h1>
<div>Thanks for signing up for <a href="https://wisp.training" target="_blank">WISP</a>. To activate your account,
please click the link below. If you didn't sign up for an
account on WISP recently, then ignore this email.</div>
<br />
<a href="https://api.wisp.training/users/${user._id}/confirmEmail/${user.confirmation.confirmationCode}" target="_blank"><button>Confirm Email</button></a>
`
};
try {
const response = await mailer.send(emailToSend);
logger.info(`Confirmation email sending response: ${response}`);
} catch (error) {
logger.error(`Unable to send confirmation email: ${error}`);
}
}
6 changes: 6 additions & 0 deletions src/validators/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ export function userValidator(method: string): ValidationChain[] {
param("userId", "Invalid or missing ':userId'").exists().isMongoId()
];
}
case "GET /users/:userId/confirmEmail/:confirmationCode": {
return [
param("userId", "Invalid or missing ':userId'").exists().isMongoId(),
param("confirmationCode", "Invalid or missing ':confirmationCode'").exists().isString()
];
}
case "POST /users": {
return [
body("username", "Invalid or missing 'username'").exists().isString(),
Expand Down