From 557ee6ad02ad5a7885b9e4cea1ff86200f53c2ae Mon Sep 17 00:00:00 2001 From: Jared Chapiewsky Date: Mon, 1 Apr 2019 19:09:00 -0500 Subject: [PATCH 01/10] set up for activation emails --- server/src/components/auth/index.ts | 2 +- server/src/components/base/baseModel.ts | 2 +- .../reset-password/passwordResetModel.ts | 2 +- server/src/components/users/index.ts | 109 +++++++++++++++++- .../components/users/userActivationModel.ts | 11 ++ server/src/components/users/userModel.ts | 4 + server/src/components/users/usersRouter.ts | 12 ++ .../20190401180528_userActivations.ts | 14 +++ .../20190401181200_addUserActivatedColumn.ts | 13 +++ .../views/emails/activation-email-html.mst | 6 + 10 files changed, 170 insertions(+), 5 deletions(-) create mode 100644 server/src/components/users/userActivationModel.ts create mode 100644 server/src/db-migrations/upgrades/20190401180528_userActivations.ts create mode 100644 server/src/db-migrations/upgrades/20190401181200_addUserActivatedColumn.ts create mode 100644 server/src/views/emails/activation-email-html.mst diff --git a/server/src/components/auth/index.ts b/server/src/components/auth/index.ts index eb9e35b..07182b1 100644 --- a/server/src/components/auth/index.ts +++ b/server/src/components/auth/index.ts @@ -34,7 +34,7 @@ export default class AuthController { // check in DB if a user with email exists or not usersController.getUserByEmail(email) .then((user) => { - if (!user) { + if (!user || user.get('activated') === false) { return done(null, false); } diff --git a/server/src/components/base/baseModel.ts b/server/src/components/base/baseModel.ts index 6f96ffc..897565f 100644 --- a/server/src/components/base/baseModel.ts +++ b/server/src/components/base/baseModel.ts @@ -1,5 +1,5 @@ import Database from "../../database"; -export default class BaseModel extends Database.bookshelf.Model { +export default class BaseModel extends Database.bookshelf.Model { } diff --git a/server/src/components/reset-password/passwordResetModel.ts b/server/src/components/reset-password/passwordResetModel.ts index 6b2b79a..c633bbb 100644 --- a/server/src/components/reset-password/passwordResetModel.ts +++ b/server/src/components/reset-password/passwordResetModel.ts @@ -3,7 +3,7 @@ import UserModel from "../users/userModel"; export default class PasswordResetModel extends BaseModel { get tableName() { return "passwordreset"; } - get idAttribute() { return "tokenhash"; } + get idAttribute() { return "token"; } get hasTimestamps() { return true; } get user() { return this.hasOne(UserModel); diff --git a/server/src/components/users/index.ts b/server/src/components/users/index.ts index 901b22a..e041bf0 100644 --- a/server/src/components/users/index.ts +++ b/server/src/components/users/index.ts @@ -4,6 +4,16 @@ import handleDatabaseErrors from "../../util/handleDatabaseErrors"; import UserModel from "./userModel"; import BaseModel from "../base/baseModel"; import {Collection, Model} from "bookshelf"; +import sendEmail from "../../util/sendEmail"; +import Mustache = require("mustache"); +import * as fs from "fs"; +import {promisify} from "util"; +import UserActivationModel from "./userActivationModel"; +import PasswordResetModel from "../reset-password/passwordResetModel"; +import * as moment from "moment"; + +// Convert the filesystem stuff to be promises +const readFile = promisify(fs.readFile); export default class UsersController { @@ -14,7 +24,7 @@ export default class UsersController { return usersCollection; } - public async getUser(id: string): Promise { + public async getUser(id: string): Promise { if (!Validator.isUUID(id, "4")) { throw new Error("Invalid ID"); } @@ -25,7 +35,7 @@ export default class UsersController { return user; } - public async getUserByEmail(email: string): Promise { + public async getUserByEmail(email: string): Promise { if (!Validator.isEmail(email)) { throw new Error("Invalid Email"); } @@ -36,6 +46,14 @@ export default class UsersController { return user; } + public async getUserByToken(token: string): Promise { + const userActivation = await new UserActivationModel() + .where({token}) + .fetch() + .catch(handleDatabaseErrors); + return await this.getUser(userActivation.get('user_id')); + } + public async createUser(data: any): Promise { // const requiredFields = ["password", "name", "email"]; // const missingRequired = validation.checkRequiredFields(requiredFields, data); @@ -66,6 +84,93 @@ export default class UsersController { .save() .catch(handleDatabaseErrors); + const userActivation = await this.createUserActivation(user.id); + + //send activation email + await this.sendActivationEmail(userActivation); + + return user; + } + + public async createUserActivation(userId: number): Promise { + const userActivation = await new UserActivationModel({user_id: userId}) + .save() + .catch(handleDatabaseErrors); + return userActivation; + } + + public async resendActivationEmail(email: string): Promise { + const user = await this.getUserByEmail(email); + if (!user) { + throw new Error("User does not exist"); + } + if (user.get('activated') === 1) { + throw new Error("User already activated"); + } + + //delete existing activation tokens + const oldUserActivation = await new UserModel() + .where({user_id : user.id}) + .destroy() + .catch(handleDatabaseErrors); + + const userActivation = await this.createUserActivation(user.id); + await this.sendActivationEmail(userActivation); + return user; } + + public async activateUser(token: string): Promise { + const user = await this.getUserByToken(token); + if (!user) { + throw new Error("Token is not valid"); + } + if (user.get('activated') === 1) { + throw new Error("User already activated"); + } + const userActivation = user.related('userActivation') as UserActivationModel; + const isExpired = moment(userActivation.get('created_at')).isBefore(moment().subtract(24, 'hours')); + if (isExpired) { + throw new Error("Activation token has expired"); + } + user.set({activated: true}).save(); + + userActivation + .destroy() + .catch(handleDatabaseErrors); + + return { + isValid: true, + user, + }; + } + + public async sendActivationEmail(userActivation: UserActivationModel): Promise { + // Load the template for the email + const template = await readFile(`${__dirname}/../../views/emails/activation-email-html.mst`, "utf8"); + const websiteName = process.env["WEBSITE_NAME"] || "FSSK"; + + const user = await this.getUser(userActivation.get('user_id')); + + // Render our reset password template + let output; + try { + output = Mustache.render(template, { + name: user.get("name"), + websiteName, + activationUrl: `${process.env["SITE_URL"]}/activate-account/${userActivation.get("token")}`, + }); + } catch (error) { + throw new Error("Could not render e-mail template because: " + error); + } + + // Send the email + await sendEmail({ + subject: `Activate Account on ${websiteName}`, + from: `"${websiteName} Support" <${process.env["SUPPORT_EMAIL"] || ""}>`, + to: [user.get("email")], + text: output, + html: output, + }); + } } diff --git a/server/src/components/users/userActivationModel.ts b/server/src/components/users/userActivationModel.ts new file mode 100644 index 0000000..4a85d4e --- /dev/null +++ b/server/src/components/users/userActivationModel.ts @@ -0,0 +1,11 @@ +import BaseModel from "../base/baseModel"; +import UserModel from "../users/userModel"; + +export default class UserActivationModel extends BaseModel { + get tableName() { return "user_activations"; } + get idAttribute() { return "token"; } + get hasTimestamps() { return true; } + get user() { + return this.belongsTo(UserModel, 'user_id'); + } +} diff --git a/server/src/components/users/userModel.ts b/server/src/components/users/userModel.ts index 37d0b1b..a6fd44a 100644 --- a/server/src/components/users/userModel.ts +++ b/server/src/components/users/userModel.ts @@ -1,8 +1,12 @@ import BaseModel from "../base/baseModel"; +import UserActivationModel from "./userActivationModel"; export default class UserModel extends BaseModel { get tableName() { return "users"; } get idAttribute() { return "id"; } get hasTimestamps() { return true; } get hidden() { return ["password"]; } // don't return the password as part of toJSON() calls + get userActivation() { + return this.hasOne(UserActivationModel); + } } diff --git a/server/src/components/users/usersRouter.ts b/server/src/components/users/usersRouter.ts index 88f4fd7..ecccb33 100644 --- a/server/src/components/users/usersRouter.ts +++ b/server/src/components/users/usersRouter.ts @@ -44,4 +44,16 @@ router.post("/register", (req: express.Request, res: express.Response, next: exp .catch((err: Error) => next(err)); }); +router.get("/resend-activation/:email", (req: express.Request, res: express.Response, next: express.NextFunction) => { + return usersController.resendActivationEmail(req.params.email) + .then((user) => res.json(user ? user.toJSON() : {})) + .catch((err: Error) => next(err)); +}); + +router.get("/activate/:token", (req: express.Request, res: express.Response, next: express.NextFunction) => { + return usersController.activateUser(req.params.token) + .then((activationResults) => res.json(activationResults ? activationResults : {isValid: false})) + .catch((err: Error) => next(err)); +}); + export default router; diff --git a/server/src/db-migrations/upgrades/20190401180528_userActivations.ts b/server/src/db-migrations/upgrades/20190401180528_userActivations.ts new file mode 100644 index 0000000..8523eab --- /dev/null +++ b/server/src/db-migrations/upgrades/20190401180528_userActivations.ts @@ -0,0 +1,14 @@ +import * as Knex from "knex"; + +exports.up = function (knex: Knex): Promise { + return Promise.resolve(knex.schema.createTableIfNotExists("user_activations", (table) => { + table.uuid("token").primary().defaultTo(knex.raw("uuid_generate_v4()")); + table.uuid("user_id").notNullable(); + table.foreign("user_id").references("users.id"); + table.timestamps(true); + })); +}; + +exports.down = function (knex: Knex): Promise { + return Promise.resolve(knex.schema.dropTable("user_activations")); +}; diff --git a/server/src/db-migrations/upgrades/20190401181200_addUserActivatedColumn.ts b/server/src/db-migrations/upgrades/20190401181200_addUserActivatedColumn.ts new file mode 100644 index 0000000..8c4e30c --- /dev/null +++ b/server/src/db-migrations/upgrades/20190401181200_addUserActivatedColumn.ts @@ -0,0 +1,13 @@ +import * as Knex from "knex"; + +exports.up = function(knex: Knex): Promise { + return Promise.resolve(knex.schema.table("users", (table) => { + table.boolean("activated").defaultTo(false); + })); +}; + +exports.down = function(knex: Knex): Promise { + return Promise.resolve(knex.schema.table("users", (table) => { + table.dropColumn("activated"); + })); +}; diff --git a/server/src/views/emails/activation-email-html.mst b/server/src/views/emails/activation-email-html.mst new file mode 100644 index 0000000..c7bae2c --- /dev/null +++ b/server/src/views/emails/activation-email-html.mst @@ -0,0 +1,6 @@ +

Hello {{name}},

+

Click the following link to confirm and activate your new account:

+ +

Activation Link

+ +

If the above link is not clickable, try copying and pasting it into the address bar of your web browser.

From 521b3b201ae68cf6ca564df7a744b59120837e60 Mon Sep 17 00:00:00 2001 From: Jared Chapiewsky Date: Tue, 2 Apr 2019 10:00:22 -0500 Subject: [PATCH 02/10] fix errors with activation stuff --- server/src/api.ts | 2 +- .../reset-password/passwordResetModel.ts | 2 +- server/src/components/todos/todoModel.ts | 2 +- server/src/components/users/index.ts | 27 ++++++++++++------- .../components/users/userActivationModel.ts | 2 +- server/src/components/users/userModel.ts | 3 ++- .../20190401180528_userActivations.ts | 1 + 7 files changed, 24 insertions(+), 15 deletions(-) diff --git a/server/src/api.ts b/server/src/api.ts index 0d8ae8b..75ec718 100644 --- a/server/src/api.ts +++ b/server/src/api.ts @@ -7,7 +7,7 @@ import todosRouter from "./components/todos/todosRouter"; const router = express.Router(); router.get("/", function(req: express.Request, res: express.Response, next: express.NextFunction) { - res.json({ hello: "world!"}); + res.json({ hello: "world?"}); }); router.use("/auth/", authRouter); diff --git a/server/src/components/reset-password/passwordResetModel.ts b/server/src/components/reset-password/passwordResetModel.ts index c633bbb..5b5c58c 100644 --- a/server/src/components/reset-password/passwordResetModel.ts +++ b/server/src/components/reset-password/passwordResetModel.ts @@ -5,7 +5,7 @@ export default class PasswordResetModel extends BaseModel { get tableName() { return "passwordreset"; } get idAttribute() { return "token"; } get hasTimestamps() { return true; } - get user() { + user() { return this.hasOne(UserModel); } } diff --git a/server/src/components/todos/todoModel.ts b/server/src/components/todos/todoModel.ts index 2f8f319..94d085e 100644 --- a/server/src/components/todos/todoModel.ts +++ b/server/src/components/todos/todoModel.ts @@ -5,7 +5,7 @@ export default class TodoModel extends BaseModel { get tableName() { return "todos"; } get idAttribute() { return "id"; } get hasTimestamps() { return true; } - get user() { + user() { return this.hasOne(UserModel); } } diff --git a/server/src/components/users/index.ts b/server/src/components/users/index.ts index e041bf0..9c426eb 100644 --- a/server/src/components/users/index.ts +++ b/server/src/components/users/index.ts @@ -46,12 +46,12 @@ export default class UsersController { return user; } - public async getUserByToken(token: string): Promise { + public async getUserActivationByToken(token: string): Promise { const userActivation = await new UserActivationModel() .where({token}) - .fetch() + .fetch({withRelated: ['user']}) .catch(handleDatabaseErrors); - return await this.getUser(userActivation.get('user_id')); + return userActivation; } public async createUser(data: any): Promise { @@ -104,14 +104,14 @@ export default class UsersController { if (!user) { throw new Error("User does not exist"); } - if (user.get('activated') === 1) { + if (user.get('activated') === true) { throw new Error("User already activated"); } - //delete existing activation tokens - const oldUserActivation = await new UserModel() + //delete any existing activation tokens + const oldUserActivation = await new UserActivationModel() .where({user_id : user.id}) - .destroy() + .destroy({require: false}) .catch(handleDatabaseErrors); const userActivation = await this.createUserActivation(user.id); @@ -121,14 +121,20 @@ export default class UsersController { } public async activateUser(token: string): Promise { - const user = await this.getUserByToken(token); + const userActivation = await this.getUserActivationByToken(token); + if (!userActivation) { + throw new Error("Token is not valid"); + } + + const user = userActivation.related('user') as UserModel; if (!user) { throw new Error("Token is not valid"); } - if (user.get('activated') === 1) { + + if (user.get('activated') === true) { throw new Error("User already activated"); } - const userActivation = user.related('userActivation') as UserActivationModel; + const isExpired = moment(userActivation.get('created_at')).isBefore(moment().subtract(24, 'hours')); if (isExpired) { throw new Error("Activation token has expired"); @@ -142,6 +148,7 @@ export default class UsersController { return { isValid: true, user, + userActivation }; } diff --git a/server/src/components/users/userActivationModel.ts b/server/src/components/users/userActivationModel.ts index 4a85d4e..49b7368 100644 --- a/server/src/components/users/userActivationModel.ts +++ b/server/src/components/users/userActivationModel.ts @@ -5,7 +5,7 @@ export default class UserActivationModel extends BaseModel { get tableName() { return "user_activations"; } get idAttribute() { return "token"; } get hasTimestamps() { return true; } - get user() { + user() { return this.belongsTo(UserModel, 'user_id'); } } diff --git a/server/src/components/users/userModel.ts b/server/src/components/users/userModel.ts index a6fd44a..5ed2db6 100644 --- a/server/src/components/users/userModel.ts +++ b/server/src/components/users/userModel.ts @@ -6,7 +6,8 @@ export default class UserModel extends BaseModel { get idAttribute() { return "id"; } get hasTimestamps() { return true; } get hidden() { return ["password"]; } // don't return the password as part of toJSON() calls - get userActivation() { + + userActivation() { return this.hasOne(UserActivationModel); } } diff --git a/server/src/db-migrations/upgrades/20190401180528_userActivations.ts b/server/src/db-migrations/upgrades/20190401180528_userActivations.ts index 8523eab..13761b2 100644 --- a/server/src/db-migrations/upgrades/20190401180528_userActivations.ts +++ b/server/src/db-migrations/upgrades/20190401180528_userActivations.ts @@ -5,6 +5,7 @@ exports.up = function (knex: Knex): Promise { table.uuid("token").primary().defaultTo(knex.raw("uuid_generate_v4()")); table.uuid("user_id").notNullable(); table.foreign("user_id").references("users.id"); + table.unique(['user_id']); table.timestamps(true); })); }; From ee4eb34c8dabddee30930040266f35b40553e921 Mon Sep 17 00:00:00 2001 From: Jared Chapiewsky Date: Wed, 3 Apr 2019 14:41:04 -0500 Subject: [PATCH 03/10] add express-brute, fix activation logic --- README.md | 2 ++ server/package.json | 7 +++++-- server/src/components/users/usersRouter.ts | 23 ++++++++++++++++++++-- 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 1d3557f..000957a 100644 --- a/README.md +++ b/README.md @@ -185,6 +185,8 @@ Update the line endings of any files that are crlf to lf and try again. In order for file changes to be picked up by the watchers in client side code, be sure to set `CHOKIDAR_USEPOLLING=true` in the `.env` file. +For the server, update the npm start command in package.json to use legacy polling: `"start": "nodemon -L",` + ### Running without docker You should be able to run the site locally without docker if desired. Make diff --git a/server/package.json b/server/package.json index 6bc2512..e51639b 100644 --- a/server/package.json +++ b/server/package.json @@ -32,10 +32,11 @@ "@types/body-parser": "1.16.7", "@types/bookshelf": "0.9.9", "@types/express": "4.16.0", + "@types/express-brute": "0.0.37", "@types/jest": "23.3.9", "@types/knex": "0.15.1", - "@types/node": "8.0.47", "@types/mustache": "0.8.32", + "@types/node": "8.0.47", "@types/passport": "0.4.6", "@types/validator": "9.4.1", "jest": "23.6.0", @@ -51,9 +52,11 @@ "bcrypt": "3.0.2", "body-parser": "1.18.3", "bookshelf": "0.13.3", + "brute-knex": "3.0.1", "connect-session-knex": "1.4.0", "dotenv": "6.0.0", "express": "4.16.3", + "express-brute": "1.0.1", "express-session": "1.15.6", "knex": "0.15.2", "moment": "2.22.2", @@ -68,7 +71,7 @@ }, "scripts": { "prestart": "npm install", - "start": "nodemon", + "start": "nodemon -L", "start:prod": "node -r dotenv/config bin/www", "build": "tsc", "lint": "tslint src/**/*.ts{,x}", diff --git a/server/src/components/users/usersRouter.ts b/server/src/components/users/usersRouter.ts index ecccb33..eaa601c 100644 --- a/server/src/components/users/usersRouter.ts +++ b/server/src/components/users/usersRouter.ts @@ -2,11 +2,21 @@ import * as express from "express"; import {adminAuthMiddleware, authMiddleware} from "../auth"; import UsersController from "./index"; import TodosController from "../todos/index"; +import Database from "../../database"; +import * as ExpressBrute from "express-brute"; +import * as BruteKnex from "brute-knex"; +import * as Passport from "passport"; const router = express.Router(); const usersController = new UsersController(); const todosController = new TodosController(); +const store = new BruteKnex({ + createTable: true, + knex: Database.knex +}); +const bruteforce = new ExpressBrute(store, { freeRetries: 2, minWait: 300000 }); + // only admin users may access this route router.get("/", adminAuthMiddleware, (req: express.Request, res: express.Response, next: express.NextFunction) => { return usersController.getUsers() @@ -44,7 +54,7 @@ router.post("/register", (req: express.Request, res: express.Response, next: exp .catch((err: Error) => next(err)); }); -router.get("/resend-activation/:email", (req: express.Request, res: express.Response, next: express.NextFunction) => { +router.get("/resend-activation/:email", bruteforce.prevent, (req: express.Request, res: express.Response, next: express.NextFunction) => { return usersController.resendActivationEmail(req.params.email) .then((user) => res.json(user ? user.toJSON() : {})) .catch((err: Error) => next(err)); @@ -52,7 +62,16 @@ router.get("/resend-activation/:email", (req: express.Request, res: express.Resp router.get("/activate/:token", (req: express.Request, res: express.Response, next: express.NextFunction) => { return usersController.activateUser(req.params.token) - .then((activationResults) => res.json(activationResults ? activationResults : {isValid: false})) + .then((activationResults) => { + if (activationResults && activationResults.isValid && activationResults.user) { + req.login(activationResults.user, function(err) { + if (err) { return next(err); } + res.json(activationResults); + }); + } else { + res.json({isValid: false}); + } + }) .catch((err: Error) => next(err)); }); From 62a1a7da38c2689d511168c50691d85124d1478e Mon Sep 17 00:00:00 2001 From: Jared Chapiewsky Date: Wed, 3 Apr 2019 17:27:37 -0500 Subject: [PATCH 04/10] Update code to send an appropriate error when user not activated --- server/src/components/auth/authRouter.ts | 3 +++ server/src/components/auth/index.ts | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/server/src/components/auth/authRouter.ts b/server/src/components/auth/authRouter.ts index 5b08965..5a16dd5 100644 --- a/server/src/components/auth/authRouter.ts +++ b/server/src/components/auth/authRouter.ts @@ -22,6 +22,9 @@ router.post("/", (req: any, res: express.Response, next: express.NextFunction) = Passport.authenticate("login", (err, user) => { if (err) { return next(err); } if (!user) { return next(new Error("login failed")); } + if (user.get('activated') === false) { + return next(new Error("user not activated")); + } req.logIn(user, (loginErr) => { if (loginErr) { return next(loginErr); diff --git a/server/src/components/auth/index.ts b/server/src/components/auth/index.ts index 07182b1..570948a 100644 --- a/server/src/components/auth/index.ts +++ b/server/src/components/auth/index.ts @@ -79,7 +79,7 @@ export default class AuthController { }); Passport.deserializeUser((id: any, done) => { - console.log("poo", id); + console.log("deserializeUser", id); usersController.getUser(id) .then((user) => { done(null, user || {}); From a90597b3d16f8aa154a1b9605f4f7831a56c460e Mon Sep 17 00:00:00 2001 From: Jared Chapiewsky Date: Wed, 3 Apr 2019 17:29:18 -0500 Subject: [PATCH 05/10] remove windows specific debugging change --- server/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/package.json b/server/package.json index e51639b..4fd1e9e 100644 --- a/server/package.json +++ b/server/package.json @@ -71,7 +71,7 @@ }, "scripts": { "prestart": "npm install", - "start": "nodemon -L", + "start": "nodemon", "start:prod": "node -r dotenv/config bin/www", "build": "tsc", "lint": "tslint src/**/*.ts{,x}", From 1ee423294d52ea68012ff939b1a6391755fd4ba8 Mon Sep 17 00:00:00 2001 From: Jared Chapiewsky Date: Thu, 4 Apr 2019 07:50:58 -0500 Subject: [PATCH 06/10] Fix failing test --- server/src/components/reset-password/passwordResetModel.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/components/reset-password/passwordResetModel.ts b/server/src/components/reset-password/passwordResetModel.ts index 5b5c58c..6a1319c 100644 --- a/server/src/components/reset-password/passwordResetModel.ts +++ b/server/src/components/reset-password/passwordResetModel.ts @@ -3,7 +3,7 @@ import UserModel from "../users/userModel"; export default class PasswordResetModel extends BaseModel { get tableName() { return "passwordreset"; } - get idAttribute() { return "token"; } + get idAttribute() { return "tokenhash"; } get hasTimestamps() { return true; } user() { return this.hasOne(UserModel); From 71fcd7f75c1bb22dbce65e57940955a0bf398b7a Mon Sep 17 00:00:00 2001 From: Jared Chapiewsky Date: Thu, 2 May 2019 09:54:31 -0500 Subject: [PATCH 07/10] return user if account is not activated so correct error message is displayed --- server/src/components/auth/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/components/auth/index.ts b/server/src/components/auth/index.ts index 570948a..64321b3 100644 --- a/server/src/components/auth/index.ts +++ b/server/src/components/auth/index.ts @@ -34,7 +34,7 @@ export default class AuthController { // check in DB if a user with email exists or not usersController.getUserByEmail(email) .then((user) => { - if (!user || user.get('activated') === false) { + if (!user) { return done(null, false); } From 6fce33d21a438571b9d7e826959b6a34bf8de17d Mon Sep 17 00:00:00 2001 From: Steve Brudz Date: Mon, 13 May 2019 11:32:12 -0500 Subject: [PATCH 08/10] Fix lint errors --- server/package-lock.json | 89 +++++++++++++++++-- server/src/components/auth/authRouter.ts | 2 +- .../reset-password/passwordResetModel.ts | 2 +- server/src/components/todos/todoModel.ts | 2 +- server/src/components/users/index.ts | 18 ++-- .../components/users/userActivationModel.ts | 4 +- server/src/components/users/userModel.ts | 2 +- server/src/components/users/usersRouter.ts | 5 +- .../20190401180528_userActivations.ts | 6 +- 9 files changed, 105 insertions(+), 25 deletions(-) diff --git a/server/package-lock.json b/server/package-lock.json index ad63be7..6e1599c 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -117,6 +117,15 @@ "@types/serve-static": "*" } }, + "@types/express-brute": { + "version": "0.0.37", + "resolved": "https://registry.npmjs.org/@types/express-brute/-/express-brute-0.0.37.tgz", + "integrity": "sha512-N1eDsPeRadiaLLfcCOxtQWSNN7ipdrg7IvRZNzQaUTvVvfGYwu8eZHOBbRrYb632jooc2Mmzz+TovBm+44itFA==", + "dev": true, + "requires": { + "@types/express": "*" + } + }, "@types/express-serve-static-core": { "version": "4.16.1", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.16.1.tgz", @@ -1317,6 +1326,60 @@ } } }, + "brute-knex": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/brute-knex/-/brute-knex-3.0.1.tgz", + "integrity": "sha512-245ygt7gNryQ/r/S/eLQiYqXAYyhFTeqHdzOpZGZakpgRhDg83/kNrTulxkSjCa9HPKT4ILoi5j0GiUrXDziDw==", + "requires": { + "express-brute": "^1.0.0", + "knex": "0.14.6" + }, + "dependencies": { + "chalk": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.2.tgz", + "integrity": "sha512-ZM4j2/ld/YZDc3Ma8PgN7gyAk+kHMMMyzLNryCPGhWrsfAuDVeuid5bpRFTDgMH9JBK2lA4dyyAkkZYF/WcqDQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "knex": { + "version": "0.14.6", + "resolved": "https://registry.npmjs.org/knex/-/knex-0.14.6.tgz", + "integrity": "sha512-A+iP8oSSmEF3JbSMfUGuJveqduDMEgyS5E/dO0ycVzAT4EE5askfunk7+37+hPqC951vnbFK/fIiNDaJIjVW0w==", + "requires": { + "babel-runtime": "^6.26.0", + "bluebird": "^3.5.1", + "chalk": "2.3.2", + "commander": "^2.15.1", + "debug": "3.1.0", + "inherits": "~2.0.3", + "interpret": "^1.1.0", + "liftoff": "2.5.0", + "lodash": "^4.17.5", + "minimist": "1.2.0", + "mkdirp": "^0.5.1", + "pg-connection-string": "2.0.0", + "readable-stream": "2.3.6", + "safe-buffer": "^5.1.1", + "tarn": "^1.1.4", + "tildify": "1.2.0", + "uuid": "^3.2.1", + "v8flags": "^3.0.2" + } + } + } + }, "bser": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/bser/-/bser-2.0.0.tgz", @@ -1651,8 +1714,7 @@ "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, "cpx": { "version": "1.5.0", @@ -2415,6 +2477,15 @@ } } }, + "express-brute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/express-brute/-/express-brute-1.0.1.tgz", + "integrity": "sha1-nzbRB/405ApoJZPjm//MUxArUzU=", + "requires": { + "long-timeout": "~0.1.1", + "underscore": "~1.8.3" + } + }, "express-session": { "version": "1.15.6", "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.15.6.tgz", @@ -5254,6 +5325,11 @@ "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", "dev": true }, + "long-timeout": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/long-timeout/-/long-timeout-0.1.1.tgz", + "integrity": "sha1-lyHXiLR+C8taJMLivuGg2lXatRQ=" + }, "loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -6308,8 +6384,7 @@ "process-nextick-args": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", - "dev": true + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" }, "prompts": { "version": "0.1.14", @@ -6466,7 +6541,6 @@ "version": "2.3.6", "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -7804,6 +7878,11 @@ "debug": "^2.2.0" } }, + "underscore": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz", + "integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI=" + }, "union-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", diff --git a/server/src/components/auth/authRouter.ts b/server/src/components/auth/authRouter.ts index 5a16dd5..71a5799 100644 --- a/server/src/components/auth/authRouter.ts +++ b/server/src/components/auth/authRouter.ts @@ -22,7 +22,7 @@ router.post("/", (req: any, res: express.Response, next: express.NextFunction) = Passport.authenticate("login", (err, user) => { if (err) { return next(err); } if (!user) { return next(new Error("login failed")); } - if (user.get('activated') === false) { + if (user.get("activated") === false) { return next(new Error("user not activated")); } req.logIn(user, (loginErr) => { diff --git a/server/src/components/reset-password/passwordResetModel.ts b/server/src/components/reset-password/passwordResetModel.ts index 6a1319c..a09494d 100644 --- a/server/src/components/reset-password/passwordResetModel.ts +++ b/server/src/components/reset-password/passwordResetModel.ts @@ -5,7 +5,7 @@ export default class PasswordResetModel extends BaseModel { get tableName() { return "passwordreset"; } get idAttribute() { return "tokenhash"; } get hasTimestamps() { return true; } - user() { + public user() { return this.hasOne(UserModel); } } diff --git a/server/src/components/todos/todoModel.ts b/server/src/components/todos/todoModel.ts index 94d085e..b70b30d 100644 --- a/server/src/components/todos/todoModel.ts +++ b/server/src/components/todos/todoModel.ts @@ -5,7 +5,7 @@ export default class TodoModel extends BaseModel { get tableName() { return "todos"; } get idAttribute() { return "id"; } get hasTimestamps() { return true; } - user() { + public user() { return this.hasOne(UserModel); } } diff --git a/server/src/components/users/index.ts b/server/src/components/users/index.ts index 9c426eb..35ea6d5 100644 --- a/server/src/components/users/index.ts +++ b/server/src/components/users/index.ts @@ -49,7 +49,7 @@ export default class UsersController { public async getUserActivationByToken(token: string): Promise { const userActivation = await new UserActivationModel() .where({token}) - .fetch({withRelated: ['user']}) + .fetch({withRelated: ["user"]}) .catch(handleDatabaseErrors); return userActivation; } @@ -86,7 +86,7 @@ export default class UsersController { const userActivation = await this.createUserActivation(user.id); - //send activation email + // send activation email await this.sendActivationEmail(userActivation); return user; @@ -104,11 +104,11 @@ export default class UsersController { if (!user) { throw new Error("User does not exist"); } - if (user.get('activated') === true) { + if (user.get("activated") === true) { throw new Error("User already activated"); } - //delete any existing activation tokens + // delete any existing activation tokens const oldUserActivation = await new UserActivationModel() .where({user_id : user.id}) .destroy({require: false}) @@ -126,16 +126,16 @@ export default class UsersController { throw new Error("Token is not valid"); } - const user = userActivation.related('user') as UserModel; + const user = userActivation.related("user") as UserModel; if (!user) { throw new Error("Token is not valid"); } - if (user.get('activated') === true) { + if (user.get("activated") === true) { throw new Error("User already activated"); } - const isExpired = moment(userActivation.get('created_at')).isBefore(moment().subtract(24, 'hours')); + const isExpired = moment(userActivation.get("created_at")).isBefore(moment().subtract(24, "hours")); if (isExpired) { throw new Error("Activation token has expired"); } @@ -148,7 +148,7 @@ export default class UsersController { return { isValid: true, user, - userActivation + userActivation, }; } @@ -157,7 +157,7 @@ export default class UsersController { const template = await readFile(`${__dirname}/../../views/emails/activation-email-html.mst`, "utf8"); const websiteName = process.env["WEBSITE_NAME"] || "FSSK"; - const user = await this.getUser(userActivation.get('user_id')); + const user = await this.getUser(userActivation.get("user_id")); // Render our reset password template let output; diff --git a/server/src/components/users/userActivationModel.ts b/server/src/components/users/userActivationModel.ts index 49b7368..bf26446 100644 --- a/server/src/components/users/userActivationModel.ts +++ b/server/src/components/users/userActivationModel.ts @@ -5,7 +5,7 @@ export default class UserActivationModel extends BaseModel { get tableName() { return "user_activations"; } get idAttribute() { return "token"; } get hasTimestamps() { return true; } - user() { - return this.belongsTo(UserModel, 'user_id'); + public user() { + return this.belongsTo(UserModel, "user_id"); } } diff --git a/server/src/components/users/userModel.ts b/server/src/components/users/userModel.ts index 5ed2db6..d2a6206 100644 --- a/server/src/components/users/userModel.ts +++ b/server/src/components/users/userModel.ts @@ -7,7 +7,7 @@ export default class UserModel extends BaseModel { get hasTimestamps() { return true; } get hidden() { return ["password"]; } // don't return the password as part of toJSON() calls - userActivation() { + public userActivation() { return this.hasOne(UserActivationModel); } } diff --git a/server/src/components/users/usersRouter.ts b/server/src/components/users/usersRouter.ts index eaa601c..ab9babe 100644 --- a/server/src/components/users/usersRouter.ts +++ b/server/src/components/users/usersRouter.ts @@ -13,7 +13,7 @@ const todosController = new TodosController(); const store = new BruteKnex({ createTable: true, - knex: Database.knex + knex: Database.knex, }); const bruteforce = new ExpressBrute(store, { freeRetries: 2, minWait: 300000 }); @@ -54,7 +54,8 @@ router.post("/register", (req: express.Request, res: express.Response, next: exp .catch((err: Error) => next(err)); }); -router.get("/resend-activation/:email", bruteforce.prevent, (req: express.Request, res: express.Response, next: express.NextFunction) => { +router.get("/resend-activation/:email", bruteforce.prevent, + (req: express.Request, res: express.Response, next: express.NextFunction) => { return usersController.resendActivationEmail(req.params.email) .then((user) => res.json(user ? user.toJSON() : {})) .catch((err: Error) => next(err)); diff --git a/server/src/db-migrations/upgrades/20190401180528_userActivations.ts b/server/src/db-migrations/upgrades/20190401180528_userActivations.ts index 13761b2..802ab12 100644 --- a/server/src/db-migrations/upgrades/20190401180528_userActivations.ts +++ b/server/src/db-migrations/upgrades/20190401180528_userActivations.ts @@ -1,15 +1,15 @@ import * as Knex from "knex"; -exports.up = function (knex: Knex): Promise { +exports.up = function(knex: Knex): Promise { return Promise.resolve(knex.schema.createTableIfNotExists("user_activations", (table) => { table.uuid("token").primary().defaultTo(knex.raw("uuid_generate_v4()")); table.uuid("user_id").notNullable(); table.foreign("user_id").references("users.id"); - table.unique(['user_id']); + table.unique(["user_id"]); table.timestamps(true); })); }; -exports.down = function (knex: Knex): Promise { +exports.down = function(knex: Knex): Promise { return Promise.resolve(knex.schema.dropTable("user_activations")); }; From cc28cf976cca19c2fd6148cfcdc24eaa58796c94 Mon Sep 17 00:00:00 2001 From: Steve Brudz Date: Mon, 13 May 2019 11:33:03 -0500 Subject: [PATCH 09/10] Update submodule pointer to use version of client code with latest fixes --- client | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client b/client index 84c833c..86db398 160000 --- a/client +++ b/client @@ -1 +1 @@ -Subproject commit 84c833c3586a64db1665f0efbec5f7cc2b8c62c0 +Subproject commit 86db398986e4e083041fd7d3ecc8c357bc1c5e0d From a72d3ff27e2d9bc3fcd762a9d8c2198d1da036d1 Mon Sep 17 00:00:00 2001 From: Steve Brudz Date: Mon, 13 May 2019 11:58:22 -0500 Subject: [PATCH 10/10] Update submodule point to pick up lint fixes --- client | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client b/client index 86db398..d7d2b49 160000 --- a/client +++ b/client @@ -1 +1 @@ -Subproject commit 86db398986e4e083041fd7d3ecc8c357bc1c5e0d +Subproject commit d7d2b491d77da73eed629909d0bf15582d43445c