Skip to content
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
89 changes: 84 additions & 5 deletions server/package-lock.json

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

5 changes: 4 additions & 1 deletion server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion server/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
3 changes: 3 additions & 0 deletions server/src/components/auth/authRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion server/src/components/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 || {});
Expand Down
2 changes: 1 addition & 1 deletion server/src/components/base/baseModel.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Database from "../../database";

export default class BaseModel extends Database.bookshelf.Model<BaseModel> {
export default class BaseModel extends Database.bookshelf.Model<any> {

}
2 changes: 1 addition & 1 deletion server/src/components/reset-password/passwordResetModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export default class PasswordResetModel extends BaseModel {
get tableName() { return "passwordreset"; }
get idAttribute() { return "tokenhash"; }
get hasTimestamps() { return true; }
get user() {
public user() {
return this.hasOne(UserModel);
}
}
2 changes: 1 addition & 1 deletion server/src/components/todos/todoModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export default class TodoModel extends BaseModel {
get tableName() { return "todos"; }
get idAttribute() { return "id"; }
get hasTimestamps() { return true; }
get user() {
public user() {
return this.hasOne(UserModel);
}
}
116 changes: 114 additions & 2 deletions server/src/components/users/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand All @@ -14,7 +24,7 @@ export default class UsersController {
return usersCollection;
}

public async getUser(id: string): Promise<BaseModel | void> {
public async getUser(id: string): Promise<UserModel> {
if (!Validator.isUUID(id, "4")) {
throw new Error("Invalid ID");
}
Expand All @@ -25,7 +35,7 @@ export default class UsersController {
return user;
}

public async getUserByEmail(email: string): Promise<BaseModel | void> {
public async getUserByEmail(email: string): Promise<UserModel> {
if (!Validator.isEmail(email)) {
throw new Error("Invalid Email");
}
Expand All @@ -36,6 +46,14 @@ export default class UsersController {
return user;
}

public async getUserActivationByToken(token: string): Promise<UserActivationModel> {
const userActivation = await new UserActivationModel()
.where({token})
.fetch({withRelated: ["user"]})
.catch(handleDatabaseErrors);
return userActivation;
}

public async createUser(data: any): Promise<BaseModel | void> {
// const requiredFields = ["password", "name", "email"];
// const missingRequired = validation.checkRequiredFields(requiredFields, data);
Expand Down Expand Up @@ -66,6 +84,100 @@ 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<UserActivationModel> {
const userActivation = await new UserActivationModel({user_id: userId})
.save()
.catch(handleDatabaseErrors);
return userActivation;
}

public async resendActivationEmail(email: string): Promise<UserModel> {
const user = await this.getUserByEmail(email);
if (!user) {
throw new Error("User does not exist");
}
if (user.get("activated") === true) {
throw new Error("User already activated");
}

// delete any existing activation tokens
const oldUserActivation = await new UserActivationModel()
.where({user_id : user.id})
.destroy({require: false})
.catch(handleDatabaseErrors);

const userActivation = await this.createUserActivation(user.id);
await this.sendActivationEmail(userActivation);

return user;
}

public async activateUser(token: string): Promise<any> {
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") === true) {
throw new Error("User already activated");
}

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,
userActivation,
};
}

public async sendActivationEmail(userActivation: UserActivationModel): Promise<BaseModel | void> {
// 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,
});
}
}
11 changes: 11 additions & 0 deletions server/src/components/users/userActivationModel.ts
Original file line number Diff line number Diff line change
@@ -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; }
public user() {
return this.belongsTo(UserModel, "user_id");
}
}
Loading