From ff25c29c6801b1858ce6cd15a5735dba1fc67e8c Mon Sep 17 00:00:00 2001 From: madelinegr Date: Thu, 14 Feb 2019 00:22:11 -0500 Subject: initial signup / login implementation --- src/server/authentication/config/passport.ts | 49 ++++++++++++ src/server/authentication/controllers/user.ts | 107 ++++++++++++++++++++++++++ src/server/authentication/models/User.ts | 76 ++++++++++++++++++ src/server/index.js | 2 + src/server/index.ts | 35 +++++++-- 5 files changed, 264 insertions(+), 5 deletions(-) create mode 100644 src/server/authentication/config/passport.ts create mode 100644 src/server/authentication/controllers/user.ts create mode 100644 src/server/authentication/models/User.ts (limited to 'src/server') diff --git a/src/server/authentication/config/passport.ts b/src/server/authentication/config/passport.ts new file mode 100644 index 000000000..05f6c3133 --- /dev/null +++ b/src/server/authentication/config/passport.ts @@ -0,0 +1,49 @@ +import * as passport from 'passport' +import * as passportLocal from 'passport-local'; +import * as mongodb from 'mongodb'; +import * as _ from "lodash"; +import { default as User } from '../models/User'; +import { Request, Response, NextFunction } from "express"; + +const LocalStrategy = passportLocal.Strategy; + +passport.serializeUser((user, done) => { + done(undefined, user.id); +}); + +passport.deserializeUser((id, done) => { + User.findById(id, (err, user) => { + done(err, user); + }); +}); + +// AUTHENTICATE JUST WITH EMAIL AND PASSWORD +passport.use(new LocalStrategy({ usernameField: 'email' }, (email, password, done) => { + User.findOne({ email: email.toLowerCase() }, (error: any, user: any) => { + if (error) return done(error); + if (!user) return done(undefined, false, { message: "Invalid email or password" }) // invalid email + user.comparePassword(password, (error: Error, isMatch: boolean) => { + if (error) return done(error); + if (!isMatch) return done(undefined, false, { message: "Invalid email or password" }); // invalid password + // valid authentication HERE + return done(undefined, user); + }); + }); +})); + +export let isAuthenticated = (req: Request, res: Response, next: NextFunction) => { + if (req.isAuthenticated()) { + return next(); + } + return res.redirect("/login"); +} + +export let isAuthorized = (req: Request, res: Response, next: NextFunction) => { + const provider = req.path.split("/").slice(-1)[0]; + + if (_.find(req.user.tokens, { kind: provider })) { + next(); + } else { + res.redirect(`/auth/${provider}`); + } +}; \ No newline at end of file diff --git a/src/server/authentication/controllers/user.ts b/src/server/authentication/controllers/user.ts new file mode 100644 index 000000000..590224fb0 --- /dev/null +++ b/src/server/authentication/controllers/user.ts @@ -0,0 +1,107 @@ +import { default as User, UserModel, AuthToken } from "../models/User"; +import { Request, Response, NextFunction } from "express"; +import * as passport from "passport"; +import { IVerifyOptions } from "passport-local"; +import "../config/passport"; +import * as request from "express-validator"; +const flash = require("express-flash"); +import * as session from "express-session"; +import * as pug from 'pug'; + +/** + * GET /signup + * Signup page. + */ +export let getSignup = (req: Request, res: Response) => { + if (req.user) { + return res.redirect("/"); + } + res.render("signup.pug", { + title: "Create Account" + }); +}; + +/** + * POST /signup + * Create a new local account. + */ +export let postSignup = (req: Request, res: Response, next: NextFunction) => { + req.assert("email", "Email is not valid").isEmail(); + req.assert("password", "Password must be at least 4 characters long").len({ min: 4 }); + req.assert("confirmPassword", "Passwords do not match").equals(req.body.password); + req.sanitize("email").normalizeEmail({ gmail_remove_dots: false }); + + const errors = req.validationErrors(); + + if (errors) { + req.flash("errors", "Unable to facilitate sign up. Please try again."); + return res.redirect("/signup"); + } + + const user = new User({ + email: req.body.email, + password: req.body.password + }); + + User.findOne({ email: req.body.email }, (err, existingUser) => { + if (err) { return next(err); } + if (existingUser) { + req.flash("errors", "Account with that email address already exists."); + return res.redirect("/signup"); + } + user.save((err) => { + if (err) { return next(err); } + req.logIn(user, (err) => { + if (err) { + return next(err); + } + res.redirect("/"); + }); + }); + }); +}; + + +/** + * GET /login + * Login page. + */ +export let getLogin = (req: Request, res: Response) => { + if (req.user) { + return res.redirect("/"); + } + res.send("

dear lord please render

"); + // res.render("account/login", { + // title: "Login" + // }); +}; + +/** + * POST /login + * Sign in using email and password. + */ +export let postLogin = (req: Request, res: Response, next: NextFunction) => { + req.assert("email", "Email is not valid").isEmail(); + req.assert("password", "Password cannot be blank").notEmpty(); + req.sanitize("email").normalizeEmail({ gmail_remove_dots: false }); + + const errors = req.validationErrors(); + + if (errors) { + req.flash("errors", "Unable to login at this time. Please try again."); + return res.redirect("/login"); + } + + passport.authenticate("local", (err: Error, user: UserModel, info: IVerifyOptions) => { + if (err) { return next(err); } + if (!user) { + req.flash("errors", info.message); + return res.redirect("/login"); + } + req.logIn(user, (err) => { + if (err) { return next(err); } + req.flash("success", "Success! You are logged in."); + res.redirect("/"); + }); + })(req, res, next); +}; \ No newline at end of file diff --git a/src/server/authentication/models/User.ts b/src/server/authentication/models/User.ts new file mode 100644 index 000000000..9f58d317d --- /dev/null +++ b/src/server/authentication/models/User.ts @@ -0,0 +1,76 @@ +import * as bcrypt from "bcrypt-nodejs"; +import * as crypto from "crypto"; +import * as mongoose from "mongoose"; + +export type UserModel = mongoose.Document & { + email: string, + password: string, + passwordResetToken: string, + passwordResetExpires: Date, + tokens: AuthToken[], + + profile: { + name: string, + gender: string, + location: string, + website: string, + picture: string + }, + + comparePassword: comparePasswordFunction, + gravatar: (size: number) => string +}; + +type comparePasswordFunction = (candidatePassword: string, cb: (err: any, isMatch: any) => {}) => void; + +export type AuthToken = { + accessToken: string, + kind: string +}; + +const userSchema = new mongoose.Schema({ + email: { type: String, unique: true }, + password: String, + passwordResetToken: String, + passwordResetExpires: Date, + + facebook: String, + twitter: String, + google: String, + tokens: Array, + + profile: { + name: String, + gender: String, + location: String, + website: String, + picture: String + } +}, { timestamps: true }); + +/** + * Password hash middleware. + */ +userSchema.pre("save", function save(next) { + const user = this as UserModel; + if (!user.isModified("password")) { return next(); } + bcrypt.genSalt(10, (err, salt) => { + if (err) { return next(err); } + bcrypt.hash(user.password, salt, () => void {}, (err: mongoose.Error, hash) => { + if (err) { return next(err); } + user.password = hash; + next(); + }); + }); +}); + +const comparePassword: comparePasswordFunction = function (this: UserModel, candidatePassword, cb) { + bcrypt.compare(candidatePassword, this.password, (err: mongoose.Error, isMatch: boolean) => { + cb(err, isMatch); + }); +}; + +userSchema.methods.comparePassword = comparePassword; + +const User = mongoose.model("User", userSchema); +export default User; \ No newline at end of file diff --git a/src/server/index.js b/src/server/index.js index 15e763f9d..1ee6fbeef 100644 --- a/src/server/index.js +++ b/src/server/index.js @@ -3,6 +3,8 @@ exports.__esModule = true; var express = require("express"); var app = express(); var port = 8080; // default port to listen +import { Database } from './database'; + // define a route handler for the default home page app.get("/", function (req, res) { res.send("Hello world!"); diff --git a/src/server/index.ts b/src/server/index.ts index ff3e2a5eb..416eaa009 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -4,6 +4,7 @@ import * as webpack from 'webpack' import * as wdm from 'webpack-dev-middleware'; import * as whm from 'webpack-hot-middleware'; import * as path from 'path' +import * as passport from 'passport'; import { MessageStore, Message, SetFieldArgs, GetFieldArgs, Transferable } from "./Message"; import { Client } from './Client'; import { Socket } from 'socket.io'; @@ -12,11 +13,21 @@ import { ObservableMap } from 'mobx'; import { FIELD_ID, Field } from '../fields/Field'; import { Database } from './database'; import { ServerUtils } from './ServerUtil'; -import { ObjectID } from 'mongodb'; -const config = require('../../webpack.config') -const compiler = webpack(config) +import * as passportConfig from './authentication/config/passport'; +import { getLogin, postLogin, getSignup, postSignup } from './authentication/controllers/user'; +const config = require('../../webpack.config'); +const compiler = webpack(config); const port = 1050; // default port to listen const serverPort = 1234; +import expressValidator = require('express-validator'); +import expressFlash = require('express-flash'); +import bodyParser = require('body-parser'); +import c = require("crypto"); + +app.use(bodyParser()); +app.use(expressValidator()); +app.use(expressFlash()); +app.use(require('express-session')({ secret: `${c.randomBytes(64)}`, resave: true, saveUninitialized: true })); let FieldStore: ObservableMap = new ObservableMap(); @@ -52,8 +63,8 @@ server.on("connection", function (socket: Socket) { Utils.Emit(socket, MessageStore.Foo, "handshooken") Utils.AddServerHandler(socket, MessageStore.Bar, barReceived) - Utils.AddServerHandler(socket, MessageStore.SetField, setField) - Utils.AddServerHandlerCallback(socket, MessageStore.GetField, getField) + // Utils.AddServerHandler(socket, MessageStore.SetField, setField) + // Utils.AddServerHandlerCallback(socket, MessageStore.GetField, getField) }) function barReceived(guid: String) { @@ -83,5 +94,19 @@ function getField([fieldRequest, callback]: [GetFieldArgs, (field: Field) => voi } } +// initialize passport +app.use(passport.initialize()); +app.use(passport.session()); + +app.use((req, res, next) => { + res.locals.user = req.user; + next(); +}); + +app.get("/signup", getSignup); +app.post("/signup", postSignup); +app.get("/login", getLogin); +app.post("/login", postLogin); + server.listen(serverPort); console.log(`listening on port ${serverPort}`); \ No newline at end of file -- cgit v1.2.3-70-g09d2