diff options
Diffstat (limited to 'src/server')
| -rw-r--r-- | src/server/authentication/config/passport.ts | 4 | ||||
| -rw-r--r-- | src/server/authentication/controllers/WorkspacesMenu.css | 3 | ||||
| -rw-r--r-- | src/server/authentication/controllers/WorkspacesMenu.tsx | 110 | ||||
| -rw-r--r-- | src/server/authentication/controllers/user.ts | 107 | ||||
| -rw-r--r-- | src/server/authentication/controllers/user_controller.ts | 278 | ||||
| -rw-r--r-- | src/server/authentication/models/user_model.ts (renamed from src/server/authentication/models/User.ts) | 22 | ||||
| -rw-r--r-- | src/server/index.ts | 120 |
7 files changed, 507 insertions, 137 deletions
diff --git a/src/server/authentication/config/passport.ts b/src/server/authentication/config/passport.ts index 05f6c3133..d90bedb18 100644 --- a/src/server/authentication/config/passport.ts +++ b/src/server/authentication/config/passport.ts @@ -2,7 +2,7 @@ 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 { default as User } from '../models/user_model'; import { Request, Response, NextFunction } from "express"; const LocalStrategy = passportLocal.Strategy; @@ -18,7 +18,7 @@ passport.deserializeUser<any, any>((id, done) => { }); // AUTHENTICATE JUST WITH EMAIL AND PASSWORD -passport.use(new LocalStrategy({ usernameField: 'email' }, (email, password, done) => { +passport.use(new LocalStrategy({ usernameField: 'email', passReqToCallback: true }, (req, 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 diff --git a/src/server/authentication/controllers/WorkspacesMenu.css b/src/server/authentication/controllers/WorkspacesMenu.css new file mode 100644 index 000000000..b89039965 --- /dev/null +++ b/src/server/authentication/controllers/WorkspacesMenu.css @@ -0,0 +1,3 @@ +.ids:hover { + color: darkblue; +}
\ No newline at end of file diff --git a/src/server/authentication/controllers/WorkspacesMenu.tsx b/src/server/authentication/controllers/WorkspacesMenu.tsx new file mode 100644 index 000000000..94d168a05 --- /dev/null +++ b/src/server/authentication/controllers/WorkspacesMenu.tsx @@ -0,0 +1,110 @@ +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; +import { observable, action, configure, reaction, computed, ObservableMap, runInAction } from 'mobx'; +import { observer } from "mobx-react"; +import * as request from 'request' +import './WorkspacesMenu.css' + +export interface WorkspaceMenuProps { + active: string; + load: (workspaceId: string) => void; + new: () => string; +} + +@observer +export class WorkspacesMenu extends React.Component<WorkspaceMenuProps> { + static Instance: WorkspacesMenu; + @observable private workspacesExposed: boolean = false; + @observable private workspaceIds: Array<string> = []; + @observable private selectedWorkspaceId: string = ""; + + constructor(props: WorkspaceMenuProps) { + super(props); + WorkspacesMenu.Instance = this; + this.loadExistingWorkspace = this.loadExistingWorkspace.bind(this); + this.addNewWorkspace = this.addNewWorkspace.bind(this); + this.selectedWorkspaceId = this.props.active; + } + + @action + addNewWorkspace() { + let newId = this.props.new(); + this.selectedWorkspaceId = newId; + this.props.load(newId); + this.toggle(); + // setTimeout(action(() => { + + // }), 100); + } + + @action + loadExistingWorkspace = (e: React.MouseEvent<HTMLLIElement, MouseEvent>) => { + let id = e.currentTarget.innerHTML; + this.props.load(id); + this.selectedWorkspaceId = id; + } + + @action + toggle() { + if (this.workspacesExposed) { + this.workspacesExposed = !this.workspacesExposed; + } else { + request.get(window.location.origin + "/getAllWorkspaceIds", this.idCallback) + } + } + + @action.bound + idCallback: request.RequestCallback = (error, response, body) => { + this.workspaceIds = []; + let ids: Array<string> = JSON.parse(body) as Array<string>; + if (ids) { + for (let i = 0; i < ids.length; i++) { + this.workspaceIds.push(ids[i]); + } + this.workspacesExposed = !this.workspacesExposed; + } + } + + render() { + let p = this.props; + return ( + <div + style={{ + width: "auto", + height: "auto", + borderRadius: 5, + position: "absolute", + top: 50, + left: this.workspacesExposed ? 8 : -500, + background: "white", + border: "black solid 2px", + transition: "all 1s ease", + zIndex: 15, + padding: 10, + }} + > + <img + src="https://bit.ly/2IBBkxk" + style={{ + width: 20, + height: 20, + marginBottom: 10, + cursor: "grab" + }} + onClick={this.addNewWorkspace} + /> + {this.workspaceIds.map(s => + <li className={"ids"} + key={s} + style={{ + listStyleType: "none", + color: s === this.selectedWorkspaceId ? "darkblue" : "black", + cursor: "grab" + }} + onClick={this.loadExistingWorkspace} + >{s}</li> + )} + </div> + ); + } +}
\ No newline at end of file diff --git a/src/server/authentication/controllers/user.ts b/src/server/authentication/controllers/user.ts deleted file mode 100644 index f74ff9039..000000000 --- a/src/server/authentication/controllers/user.ts +++ /dev/null @@ -1,107 +0,0 @@ -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: "Sign Up" - }); -}; - -/** - * 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("<p>dear lord please render</p>"); - // 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/controllers/user_controller.ts b/src/server/authentication/controllers/user_controller.ts new file mode 100644 index 000000000..7b89b5152 --- /dev/null +++ b/src/server/authentication/controllers/user_controller.ts @@ -0,0 +1,278 @@ +import { default as User, DashUserModel, AuthToken } from "../models/user_model"; +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'; +import * as async from 'async'; +import * as nodemailer from 'nodemailer'; +import c = require("crypto"); + + +/** + * GET / + * Whenever a user navigates to the root of Dash + * (doesn't specify a sub-route), redirect to login. + * If the user is already signed in, it will effectively + * automatically redirect them to /home instead + */ +export let getEntry = (req: Request, res: Response) => { + res.redirect("/login"); +} + +/** + * GET /signup + * Directs user to the signup page + * modeled by signup.pug in views + */ +export let getSignup = (req: Request, res: Response) => { + if (req.user) { + let user = req.user; + return res.redirect("/home"); + } + res.render("signup.pug", { + title: "Sign Up", + user: req.user, + }); +}; + +/** + * 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) { + res.render("signup.pug", { + title: "Sign Up", + user: req.user, + }); + return res.redirect("/signup"); + } + + const email = req.body.email; + const password = req.body.password; + + const user = new User({ + email, + password, + userDoc: "document here" + }); + + User.findOne({ email }, (err, existingUser) => { + if (err) { return next(err); } + if (existingUser) { + return res.redirect("/login"); + } + user.save((err) => { + if (err) { return next(err); } + req.logIn(user, (err) => { + if (err) { + return next(err); + } + res.redirect("/home"); + }); + }); + }); + +}; + + +/** + * GET /login + * Login page. + */ +export let getLogin = (req: Request, res: Response) => { + if (req.user) { + return res.redirect("/home"); + } + res.render("login.pug", { + title: "Log In", + user: req.user + }); +}; + +/** + * POST /login + * Sign in using email and password. + * On failure, redirect to login page + */ +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("/signup"); + } + + passport.authenticate("local", (err: Error, user: DashUserModel, info: IVerifyOptions) => { + if (err) { return next(err); } + if (!user) { + return res.redirect("/signup"); + } + req.logIn(user, (err) => { + if (err) { return next(err); } + res.redirect("/home"); + }); + })(req, res, next); +}; + +/** + * GET /logout + * Invokes the logout function on the request + * and destroys the user's current session. + */ +export let getLogout = (req: Request, res: Response) => { + const dashUser: DashUserModel | undefined = req.user; + if (dashUser) { + dashUser.update({ $set: { didSelectSessionWorkspace: false } }, () => { }) + } + req.logout(); + const sess = req.session; + if (sess) { + sess.destroy((err) => { if (err) { console.log(err); } }); + } + res.redirect('/login'); +} + +export let getForgot = function (req: Request, res: Response) { + res.render("forgot.pug", { + title: "Recover Password", + user: req.user, + }); +} + +export let postForgot = function (req: Request, res: Response, next: NextFunction) { + const email = req.body.email; + async.waterfall([ + function (done: any) { + let token: string; + c.randomBytes(20, function (err: any, buffer: Buffer) { + if (err) { + done(null); + return; + } + done(null, buffer.toString('hex')); + }) + }, + function (token: string, done: any) { + User.findOne({ email }, function (err, user: DashUserModel) { + if (!user) { + // NO ACCOUNT WITH SUBMITTED EMAIL + return res.redirect('/forgot'); + } + user.passwordResetToken = token; + user.passwordResetExpires = new Date(Date.now() + 3600000); // 1 HOUR + user.save(function (err: any) { + done(null, token, user); + }); + }); + }, + function (token: Uint16Array, user: DashUserModel, done: any) { + const smtpTransport = nodemailer.createTransport({ + service: 'Gmail', + auth: { + user: 'brownptcdash@gmail.com', + pass: 'browngfx1' + } + }); + const mailOptions = { + to: user.email, + from: 'brownptcdash@gmail.com', + subject: 'Dash Password Reset', + text: 'You are receiving this because you (or someone else) have requested the reset of the password for your account.\n\n' + + 'Please click on the following link, or paste this into your browser to complete the process:\n\n' + + 'http://' + req.headers.host + '/reset/' + token + '\n\n' + + 'If you did not request this, please ignore this email and your password will remain unchanged.\n' + }; + smtpTransport.sendMail(mailOptions, function (err) { + // req.flash('info', 'An e-mail has been sent to ' + user.email + ' with further instructions.'); + done(null, err, 'done'); + }); + } + ], function (err) { + if (err) return next(err); + res.redirect('/forgot'); + }) +} + +export let getReset = function (req: Request, res: Response) { + User.findOne({ passwordResetToken: req.params.token, passwordResetExpires: { $gt: Date.now() } }, function (err, user: DashUserModel) { + if (!user || err) { + return res.redirect('/forgot'); + } + res.render("reset.pug", { + title: "Reset Password", + user: req.user, + }); + }); +} + +export let postReset = function (req: Request, res: Response) { + async.waterfall([ + function (done: any) { + User.findOne({ passwordResetToken: req.params.token, passwordResetExpires: { $gt: Date.now() } }, function (err, user: DashUserModel) { + if (!user || err) { + return res.redirect('back'); + } + + 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); + + if (req.validationErrors()) { + return res.redirect('back'); + } + + user.password = req.body.password; + user.passwordResetToken = undefined; + user.passwordResetExpires = undefined; + + user.save(function (err) { + if (err) { + return res.redirect("/login"); + } + req.logIn(user, function (err) { + if (err) { + return; + } + }); + done(null, user); + }); + }); + }, + function (user: DashUserModel, done: any) { + const smtpTransport = nodemailer.createTransport({ + service: 'Gmail', + auth: { + user: 'brownptcdash@gmail.com', + pass: 'browngfx1' + } + }); + const mailOptions = { + to: user.email, + from: 'brownptcdash@gmail.com', + subject: 'Your password has been changed', + text: 'Hello,\n\n' + + 'This is a confirmation that the password for your account ' + user.email + ' has just been changed.\n' + }; + smtpTransport.sendMail(mailOptions, function (err) { + done(null, err); + }); + } + ], function (err) { + res.redirect('/login'); + }); +}
\ No newline at end of file diff --git a/src/server/authentication/models/User.ts b/src/server/authentication/models/user_model.ts index 9752c4260..29076ba19 100644 --- a/src/server/authentication/models/User.ts +++ b/src/server/authentication/models/user_model.ts @@ -1,6 +1,5 @@ //@ts-ignore import * as bcrypt from "bcrypt-nodejs"; -import * as crypto from "crypto"; //@ts-ignore import * as mongoose from "mongoose"; var url = 'mongodb://localhost:27017/Dash' @@ -16,12 +15,14 @@ mongoose.connection.on('error', function (error) { mongoose.connection.on('disconnected', function () { console.log('connection closed'); }); -export type UserModel = mongoose.Document & { +export type DashUserModel = mongoose.Document & { email: string, password: string, - passwordResetToken: string, - passwordResetExpires: Date, - tokens: AuthToken[], + passwordResetToken: string | undefined, + passwordResetExpires: Date | undefined, + + allWorkspaceIds: Array<String>, + activeWorkspaceId: String, profile: { name: string, @@ -47,10 +48,15 @@ const userSchema = new mongoose.Schema({ passwordResetToken: String, passwordResetExpires: Date, + allWorkspaceIds: { + type: Array, + default: [] + }, + activeWorkspaceId: String, + facebook: String, twitter: String, google: String, - tokens: Array, profile: { name: String, @@ -65,7 +71,7 @@ const userSchema = new mongoose.Schema({ * Password hash middleware. */ userSchema.pre("save", function save(next) { - const user = this as UserModel; + const user = this as DashUserModel; if (!user.isModified("password")) { return next(); } bcrypt.genSalt(10, (err, salt) => { if (err) { return next(err); } @@ -77,7 +83,7 @@ userSchema.pre("save", function save(next) { }); }); -const comparePassword: comparePasswordFunction = function (this: UserModel, candidatePassword, cb) { +const comparePassword: comparePasswordFunction = function (this: DashUserModel, candidatePassword, cb) { bcrypt.compare(candidatePassword, this.password, (err: mongoose.Error, isMatch: boolean) => { cb(err, isMatch); }); diff --git a/src/server/index.ts b/src/server/index.ts index eb0527ee7..e6f08bc29 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -3,7 +3,6 @@ const app = express() 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'; @@ -14,23 +13,28 @@ import { FieldId, Field } from '../fields/Field'; import { Database } from './database'; import { ServerUtils } from './ServerUtil'; import { ObjectID } from 'mongodb'; +import * as bcrypt from "bcrypt-nodejs"; import { Document } from '../fields/Document'; import * as io from 'socket.io' import * as passportConfig from './authentication/config/passport'; -import { getLogin, postLogin, getSignup, postSignup } from './authentication/controllers/user'; +import { getLogin, postLogin, getSignup, postSignup, getLogout, getEntry, postReset, getForgot, postForgot, getReset } from './authentication/controllers/user_controller'; const config = require('../../webpack.config'); const compiler = webpack(config); const port = 1050; // default port to listen const serverPort = 1234; import * as expressValidator from 'express-validator'; import expressFlash = require('express-flash'); +import flash = require('connect-flash'); import * as bodyParser from 'body-parser'; import * as session from 'express-session'; +// import cookieSession = require('cookie-session'); +import * as cookieParser from 'cookie-parser'; import c = require("crypto"); const MongoStore = require('connect-mongo')(session); const mongoose = require('mongoose'); -const bluebird = require('bluebird'); import { performance } from 'perf_hooks' +import * as path from 'path' +import User, { DashUserModel } from './authentication/models/user_model'; import * as fs from 'fs'; import * as request from 'request' @@ -39,29 +43,34 @@ const download = (url: string, dest: fs.PathLike) => { } const mongoUrl = 'mongodb://localhost:27017/Dash'; -// mongoose.Promise = bluebird; -mongoose.connect(mongoUrl)//.then( -// () => { /** ready to use. The `mongoose.connect()` promise resolves to undefined. */ }, -// ).catch((err: any) => { -// console.log("MongoDB connection error. Please make sure MongoDB is running. " + err); -// process.exit(); -// }); +mongoose.connect(mongoUrl) mongoose.connection.on('connected', function () { console.log("connected"); }) -app.use(bodyParser.json()); -app.use(bodyParser.urlencoded({ extended: true })); -app.use(expressValidator()); -app.use(expressFlash()); -app.use(require('express-session')({ +// SESSION MANAGEMENT AND AUTHENTICATION MIDDLEWARE +// ORDER OF IMPORTS MATTERS + +app.use(cookieParser(`${c.randomBytes(64)}`)); +app.use(session({ secret: `${c.randomBytes(64)}`, resave: true, + cookie: { maxAge: 7 * 24 * 60 * 60 }, saveUninitialized: true, store: new MongoStore({ url: 'mongodb://localhost:27017/Dash' }) })); +// app.use(cookieSession({ +// name: 'authentication', +// keys: [`${c.randomBytes(8)}`, `${c.randomBytes(8)}`, `${c.randomBytes(8)}`], +// maxAge: 7 * 24 * 60 * 60 * 1000 +// })); +app.use(flash()); +app.use(expressFlash()); +app.use(bodyParser.json()); +app.use(bodyParser.urlencoded({ extended: true })); +app.use(expressValidator()); app.use(passport.initialize()); app.use(passport.session()); app.use((req, res, next) => { @@ -69,18 +78,89 @@ app.use((req, res, next) => { next(); }); +// AUTHENTICATION ROUTING + +// *** +// Look for the definitions of these get and post +// functions in the exports of user.ts + +// /home defines destination after a successful log in +app.get("/home", (req, res) => { + // if user is not logged in, redirect to log in page + const dashUser: DashUserModel = req.user; + if (!dashUser) { + return res.redirect("/login"); + } + // otherwise, connect them to Dash + // TODO: store and manage users' workspaces + // if (dashUser.allWorkspaceIds.length > 0) { + // if (!dashUser.didSelectSessionWorkspace) { + // return res.redirect("/workspaces"); + // } + // } + res.sendFile(path.join(__dirname, '../../deploy/index.html')); +}); + +// app.get("/workspaces", getWorkspaces); + +app.get("/getActiveWorkspaceId", (req, res) => { + const dashUser: DashUserModel = req.user; + if (!dashUser) { + return; + } + res.send(dashUser.activeWorkspaceId || ""); +}); + +app.get("/getAllWorkspaceIds", (req, res) => { + const dashUser: DashUserModel = req.user; + if (!dashUser) { + return; + } + res.send(JSON.stringify(dashUser.allWorkspaceIds as Array<String>)); +}) + +app.post("/setActiveWorkspaceId", (req, res) => { + const dashUser: DashUserModel = req.user; + if (!dashUser) { + return; + } + dashUser.update({ $set: { activeWorkspaceId: req.body.target } }, () => { }); +}) + +app.post("/addWorkspaceId", (req, res) => { + const dashUser: DashUserModel = req.user; + if (!dashUser) { + return; + } + dashUser.update({ $push: { allWorkspaceIds: req.body.target } }, () => { }); +}) + +// anyone attempting to navigate to localhost at this port will +// first have to login +app.get("/", getEntry); + +// Sign Up app.get("/signup", getSignup); app.post("/signup", postSignup); + +// Log In app.get("/login", getLogin); app.post("/login", postLogin); -let FieldStore: ObservableMap<FieldId, Field> = new ObservableMap(); +// Log Out +app.get('/logout', getLogout); -// define a route handler for the default home page -app.get("/", (req, res) => { - res.sendFile(path.join(__dirname, '../../deploy/index.html')); -}); +// *** +// FORGOT PASSWORD EMAIL HANDLING +app.get('/forgot', getForgot) +app.post('/forgot', postForgot) + +// RESET PASSWORD EMAIL HANDLING +app.get('/reset/:token', getReset); +app.post('/reset/:token', postReset); + +let FieldStore: ObservableMap<FieldId, Field> = new ObservableMap(); app.get("/hello", (req, res) => { res.send("<p>Hello</p>"); }) |
