diff options
author | bob <bcz@cs.brown.edu> | 2019-04-03 11:45:15 -0400 |
---|---|---|
committer | bob <bcz@cs.brown.edu> | 2019-04-03 11:45:15 -0400 |
commit | bb7d5a26ec68f283c5adb42d4d6554253de7176f (patch) | |
tree | 7efa817102ba54e916601611f608dd4bd761a437 /src/server/authentication | |
parent | 43a0768690caa89c606dd5d296d3cc8825c1702b (diff) | |
parent | c406c8d123ce0aa9d63fb8a4dd90adfe83d2889d (diff) |
merged with master
Diffstat (limited to 'src/server/authentication')
-rw-r--r-- | src/server/authentication/config/passport.ts | 7 | ||||
-rw-r--r-- | src/server/authentication/controllers/WorkspacesMenu.css | 3 | ||||
-rw-r--r-- | src/server/authentication/controllers/WorkspacesMenu.tsx | 89 | ||||
-rw-r--r-- | src/server/authentication/controllers/user.ts | 107 | ||||
-rw-r--r-- | src/server/authentication/controllers/user_controller.ts | 264 | ||||
-rw-r--r-- | src/server/authentication/models/current_user_utils.ts | 102 | ||||
-rw-r--r-- | src/server/authentication/models/user_model.ts (renamed from src/server/authentication/models/User.ts) | 17 |
7 files changed, 471 insertions, 118 deletions
diff --git a/src/server/authentication/config/passport.ts b/src/server/authentication/config/passport.ts index 05f6c3133..b6fe15655 100644 --- a/src/server/authentication/config/passport.ts +++ b/src/server/authentication/config/passport.ts @@ -2,8 +2,9 @@ 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"; +import { RouteStore } from '../../RouteStore'; const LocalStrategy = passportLocal.Strategy; @@ -18,7 +19,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 @@ -35,7 +36,7 @@ export let isAuthenticated = (req: Request, res: Response, next: NextFunction) = if (req.isAuthenticated()) { return next(); } - return res.redirect("/login"); + return res.redirect(RouteStore.login); } export let isAuthorized = (req: Request, res: Response, next: NextFunction) => { 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..8e14cf98e --- /dev/null +++ b/src/server/authentication/controllers/WorkspacesMenu.tsx @@ -0,0 +1,89 @@ +import * as React from 'react'; +import { observable, action, configure, reaction, computed, ObservableMap, runInAction } from 'mobx'; +import { observer } from "mobx-react"; +import './WorkspacesMenu.css' +import { Document } from '../../../fields/Document'; +import { EditableView } from '../../../client/views/EditableView'; +import { KeyStore } from '../../../fields/KeyStore'; + +export interface WorkspaceMenuProps { + active: Document | undefined; + open: (workspace: Document) => void; + new: () => void; + allWorkspaces: Document[]; + isShown: () => boolean; + toggle: () => void; +} + +@observer +export class WorkspacesMenu extends React.Component<WorkspaceMenuProps> { + constructor(props: WorkspaceMenuProps) { + super(props); + this.addNewWorkspace = this.addNewWorkspace.bind(this); + } + + @action + addNewWorkspace() { + this.props.new(); + this.props.toggle(); + } + + render() { + return ( + <div + style={{ + width: "auto", + maxHeight: '200px', + overflow: 'scroll', + borderRadius: 5, + position: "absolute", + top: 78, + left: this.props.isShown() ? 11 : -500, + background: "white", + border: "black solid 2px", + transition: "all 1s ease", + zIndex: 15, + padding: 10, + paddingRight: 12, + }}> + <img + src="https://bit.ly/2IBBkxk" + style={{ + width: 20, + height: 20, + marginTop: 3, + marginLeft: 3, + marginBottom: 3, + cursor: "grab" + }} + onClick={this.addNewWorkspace} + /> + {this.props.allWorkspaces.map((s, i) => + <div + key={s.Id} + onContextMenu={(e) => { + e.preventDefault(); + this.props.open(s); + }} + style={{ + marginTop: 10, + color: s === this.props.active ? "red" : "black" + }} + > + <span>{i + 1} - </span> + <EditableView + display={"inline"} + GetValue={() => { return s.Title }} + SetValue={(title: string): boolean => { + s.SetText(KeyStore.Title, title); + return true; + }} + contents={s.Title} + height={20} + /> + </div> + )} + </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..e365b8dce --- /dev/null +++ b/src/server/authentication/controllers/user_controller.ts @@ -0,0 +1,264 @@ +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"); +import { RouteStore } from "../../RouteStore"; +import { Utils } from "../../../Utils"; + +/** + * 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(RouteStore.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(RouteStore.signup); + } + + const email = req.body.email; + const password = req.body.password; + + const user = new User({ + email, + password, + userDocumentId: Utils.GenerateGuid() + }); + + User.findOne({ email }, (err, existingUser) => { + if (err) { return next(err); } + if (existingUser) { + return res.redirect(RouteStore.login); + } + user.save((err) => { + if (err) { return next(err); } + req.logIn(user, (err) => { + if (err) { + return next(err); + } + res.redirect(RouteStore.home); + }); + }); + }); + +}; + + +/** + * GET /login + * Login page. + */ +export let getLogin = (req: Request, res: Response) => { + if (req.user) { + return res.redirect(RouteStore.home); + } + res.render("login.pug", { + title: "Log In", + user: req.user + }); +}; + +/** + * POST /login + * Sign in using email and password. + * On failure, redirect to signup 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(RouteStore.signup); + } + + passport.authenticate("local", (err: Error, user: DashUserModel, info: IVerifyOptions) => { + if (err) { return next(err); } + if (!user) { + return res.redirect(RouteStore.signup); + } + req.logIn(user, (err) => { + if (err) { return next(err); } + res.redirect(RouteStore.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) => { + req.logout(); + const sess = req.session; + if (sess) { + sess.destroy((err) => { if (err) { console.log(err); } }); + } + res.redirect(RouteStore.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(RouteStore.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(RouteStore.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(RouteStore.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(RouteStore.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(RouteStore.login); + }); +}
\ No newline at end of file diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts new file mode 100644 index 000000000..0ac85b446 --- /dev/null +++ b/src/server/authentication/models/current_user_utils.ts @@ -0,0 +1,102 @@ +import { DashUserModel } from "./user_model"; +import * as rp from 'request-promise'; +import { RouteStore } from "../../RouteStore"; +import { ServerUtils } from "../../ServerUtil"; +import { Server } from "../../../client/Server"; +import { Document } from "../../../fields/Document"; +import { KeyStore } from "../../../fields/KeyStore"; +import { ListField } from "../../../fields/ListField"; +import { Documents } from "../../../client/documents/Documents"; +import { Schema, Attribute, AttributeGroup, Catalog } from "../../../client/northstar/model/idea/idea"; +import { observable, computed, action } from "mobx"; +import { ArrayUtil } from "../../../client/northstar/utils/ArrayUtil"; + +export class CurrentUserUtils { + private static curr_email: string; + private static curr_id: string; + private static user_document: Document; + //TODO tfs: these should be temporary... + private static mainDocId: string | undefined; + @observable private static catalog?: Catalog; + + public static get email(): string { + return this.curr_email; + } + + public static get id(): string { + return this.curr_id; + } + + public static get UserDocument(): Document { + return this.user_document; + } + + public static get MainDocId(): string | undefined { + return this.mainDocId; + } + + public static set MainDocId(id: string | undefined) { + this.mainDocId = id; + } + + @computed public static get NorthstarDBCatalog(): Catalog | undefined { + return this.catalog; + } + public static set NorthstarDBCatalog(ctlog: Catalog | undefined) { + this.catalog = ctlog; + } + public static GetNorthstarSchema(name: string): Schema | undefined { + return !this.catalog || !this.catalog.schemas ? undefined : + ArrayUtil.FirstOrDefault<Schema>(this.catalog.schemas, (s: Schema) => s.displayName === name); + } + public static GetAllNorthstarColumnAttributes(schema: Schema) { + if (!schema || !schema.rootAttributeGroup) { + return []; + } + const recurs = (attrs: Attribute[], g: AttributeGroup) => { + if (g.attributes) { + attrs.push.apply(attrs, g.attributes); + if (g.attributeGroups) { + g.attributeGroups.forEach(ng => recurs(attrs, ng)); + } + } + }; + const allAttributes: Attribute[] = new Array<Attribute>(); + recurs(allAttributes, schema.rootAttributeGroup); + return allAttributes; + } + + private static createUserDocument(id: string): Document { + let doc = new Document(id); + + doc.Set(KeyStore.Workspaces, new ListField<Document>()); + doc.Set(KeyStore.OptionalRightCollection, Documents.SchemaDocument([], { title: "Pending documents" })) + return doc; + } + + public static loadCurrentUser(): Promise<any> { + let userPromise = rp.get(ServerUtils.prepend(RouteStore.getCurrUser)).then((response) => { + if (response) { + let obj = JSON.parse(response); + CurrentUserUtils.curr_id = obj.id as string; + CurrentUserUtils.curr_email = obj.email as string; + } else { + throw new Error("There should be a user! Why does Dash think there isn't one?") + } + }); + let userDocPromise = rp.get(ServerUtils.prepend(RouteStore.getUserDocumentId)).then(id => { + if (id) { + return Server.GetField(id).then(field => { + if (field instanceof Document) { + this.user_document = field; + } else { + this.user_document = this.createUserDocument(id); + } + }) + } else { + throw new Error("There should be a user id! Why does Dash think there isn't one?") + } + }); + return Promise.all([userPromise, userDocPromise]); + } +}
\ 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..81580aad5 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,13 @@ 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, + + userDocumentId: string; profile: { name: string, @@ -47,10 +47,11 @@ const userSchema = new mongoose.Schema({ passwordResetToken: String, passwordResetExpires: Date, + userDocumentId: String, + facebook: String, twitter: String, google: String, - tokens: Array, profile: { name: String, @@ -65,7 +66,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 +78,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); }); |