From 8ba7388e1e14bdde62cdaec42487fa6e7c51cf50 Mon Sep 17 00:00:00 2001 From: nicholasjhall Date: Wed, 16 Jun 2021 16:53:20 -0400 Subject: [PATCH 1/3] Add option for no username in config, refactor signup --- config.js | 3 + server/api/signup.js | 13 +- server/api/users.js | 5 +- server/models/user.js | 120 ++++++++++++------ server/web/public/scripts/signup.js | 12 +- server/web/routes/index.js | 8 +- server/web/routes/signup.js | 3 +- server/web/templates/signup/signup.handlebars | 6 +- 8 files changed, 118 insertions(+), 52 deletions(-) diff --git a/config.js b/config.js index be0a86a..d18bd59 100755 --- a/config.js +++ b/config.js @@ -94,6 +94,9 @@ const config = { address: 'jedireza@gmail.com' } }, + loginInfo: { + usernameRequired: true + }, passwordComplexity: { min: 8, max: 32, diff --git a/server/api/signup.js b/server/api/signup.js index 74f637a..5128347 100755 --- a/server/api/signup.js +++ b/server/api/signup.js @@ -23,13 +23,14 @@ const register = function (server, options) { assign: 'usernameCheck', method: async function (request, h) { - const user = await User.findByUsername(request.payload.username); + if (Config.get('/loginInfo/usernameRequired')) { + const user = await User.findByUsername(request.payload.username); - if (user) { + if (user) { - throw Boom.conflict('Username already in use.'); + throw Boom.conflict('Username already in use.'); + } } - return h.continue; } }, { @@ -104,13 +105,15 @@ const register = function (server, options) { const result = { user: { _id: user._id, - username: user.username, email: user.email, roles: user.roles }, session, authHeader }; + if (Config.get('/loginInfo/usernameRequired')) { + result.user.username = user.username; + }; return result; } diff --git a/server/api/users.js b/server/api/users.js index 252461d..37284c4 100755 --- a/server/api/users.js +++ b/server/api/users.js @@ -112,7 +112,10 @@ const register = function (server, options) { }, handler: async function (request, h) { - const username = request.payload.username; + let username = null; + if (Config.get('/loginInfo/usernameRequired')) { + username = request.payload.username; + } const password = request.payload.password; const email = request.payload.email; const name = request.payload.name; diff --git a/server/models/user.js b/server/models/user.js index b03fd8a..c70164e 100755 --- a/server/models/user.js +++ b/server/models/user.js @@ -35,7 +35,9 @@ class User extends AnchorModel { static async create(username, password, email, name) { - Assert.ok(username, 'Missing username arugment.'); + if (Config.get('/loginInfo/usernameRequired')) { + Assert.ok(username, 'Missing username arugment.'); + } Assert.ok(password, 'Missing pasword arugment.'); Assert.ok(email, 'Missing email arugment.'); Assert.ok(name, 'Missing name arugment.'); @@ -43,17 +45,32 @@ class User extends AnchorModel { const self = this; const passwordHash = await this.generatePasswordHash(password); - const document = new this({ - isActive: true, - inStudy: true, - username: username.toLowerCase(), - password: passwordHash.hash, - email: email.toLowerCase(), - name, - roles: {}, - studyID: null, - timeCreated: new Date() - }); + let document; + if (Config.get('/loginInfo/usernameRequired')) { + document = new this({ + isActive: true, + inStudy: true, + username: username.toLowerCase(), + password: passwordHash.hash, + email: email.toLowerCase(), + name, + roles: {}, + studyID: null, + timeCreated: new Date() + }); + } + else { + document = new this({ + isActive: true, + inStudy: true, + password: passwordHash.hash, + email: email.toLowerCase(), + name, + roles: {}, + studyID: null, + timeCreated: new Date() + }); + } const users = await self.insertOne(document); @@ -148,29 +165,61 @@ class User extends AnchorModel { User.collectionName = 'users'; +if (Config.get('/loginInfo/usernameRequired')) { + User.schema = Joi.object({ + _id: Joi.object(), + isActive: Joi.boolean().default(true), + username: Joi.string().token().lowercase().required(), + password: Joi.string(), + name: Joi.string(), + inStudy: Joi.boolean().default(true), + email: Joi.string().email().lowercase().required(), + roles: Joi.object(getRolesValidator()), + resetPassword: Joi.object({ + token: Joi.string().required(), + expires: Joi.date().required() + }), + timeCreated: Joi.date() + }); -User.schema = Joi.object({ - _id: Joi.object(), - isActive: Joi.boolean().default(true), - username: Joi.string().token().lowercase().required(), - password: Joi.string(), - name: Joi.string(), - inStudy: Joi.boolean().default(true), - email: Joi.string().email().lowercase().required(), - roles: Joi.object(getRolesValidator()), - resetPassword: Joi.object({ - token: Joi.string().required(), - expires: Joi.date().required() - }), - timeCreated: Joi.date() -}); + User.payload = Joi.object({ + username: Joi.string().token().lowercase().invalid('root').required(), + password: Joi.string().required(), + email: Joi.string().email().lowercase().required(), + name: Joi.string().required() + }); -User.payload = Joi.object({ - username: Joi.string().token().lowercase().invalid('root').required(), - password: Joi.string().required(), - email: Joi.string().email().lowercase().required(), - name: Joi.string().required() -}); + User.indexes = [ + { key: { username: 1, unique: 1 } }, + { key: { email: 1, unique: 1 } } + ]; +} +else { + User.schema = Joi.object({ + _id: Joi.object(), + isActive: Joi.boolean().default(true), + password: Joi.string(), + name: Joi.string(), + inStudy: Joi.boolean().default(true), + email: Joi.string().email().lowercase().required(), + roles: Joi.object(getRolesValidator()), + resetPassword: Joi.object({ + token: Joi.string().required(), + expires: Joi.date().required() + }), + timeCreated: Joi.date() + }); + + User.payload = Joi.object({ + password: Joi.string().required(), + email: Joi.string().email().lowercase().required(), + name: Joi.string().required() + }); + + User.indexes = [ + { key: { email: 1, unique: 1 } } + ]; +} User.routes = Hoek.applyToDefaults(AnchorModel.routes, { create: { @@ -199,9 +248,4 @@ User.routes = Hoek.applyToDefaults(AnchorModel.routes, { } }); -User.indexes = [ - { key: { username: 1, unique: 1 } }, - { key: { email: 1, unique: 1 } } -]; - module.exports = User; diff --git a/server/web/public/scripts/signup.js b/server/web/public/scripts/signup.js index e2267c4..f094d1b 100644 --- a/server/web/public/scripts/signup.js +++ b/server/web/public/scripts/signup.js @@ -6,7 +6,15 @@ const signUpSchema = Joi.object({ password: Joi.string().required().min(8).regex(/^[A-Z]+[a-z]+[0-9]+$/, '1 Uppercase, 1 lowercase, 1 number'), confirmPassword: Joi.string().required().min(8).regex(/^[A-Z]+[a-z]+[0-9]+$/, '1 Uppercase, 1 lowercase, 1 number') }); +const signUpSchemaNoUsername = Joi.object({ + name: Joi.string().required(), + email: Joi.string().email().required(), + password: Joi.string().required().min(8).regex(/^[A-Z]+[a-z]+[0-9]+$/, '1 Uppercase, 1 lowercase, 1 number'), + confirmPassword: Joi.string().required().min(8).regex(/^[A-Z]+[a-z]+[0-9]+$/, '1 Uppercase, 1 lowercase, 1 number') +}); + joiToForm('signUpFormFields',signUpSchema); +joiToForm('signUpFormFieldsNoUsername',signUpSchemaNoUsername); $('#signup').click((event) => { event.preventDefault(); @@ -15,12 +23,12 @@ $('#signup').click((event) => { values[field.name] = field.value; }); if(values['password'] === values['confirmPassword']) { - delete values['confirmPassword']; + delete values['confirmPassword']; $.ajax({ type: 'POST', url: '/api/signup', data: values, - success: function (result) { + success: function (result) { window.location = '/'; }, error: function (result) { diff --git a/server/web/routes/index.js b/server/web/routes/index.js index 1121084..4f4a364 100755 --- a/server/web/routes/index.js +++ b/server/web/routes/index.js @@ -1,7 +1,7 @@ 'use strict'; const Config = require('../../../config'); -const PermissionConfigTable = require('../../permission-config.json'); -const DefaultScopes = require('../../helper/getRoleNames'); +// const PermissionConfigTable = require('../../permission-config.json'); +// const DefaultScopes = require('../../helper/getRoleNames'); const register = function (server, options) { @@ -10,8 +10,8 @@ const register = function (server, options) { path: '/', options: { auth: { - strategies: ['simple', 'session'], - scope: PermissionConfigTable.GET['/'] || DefaultScopes + strategies: ['simple', 'session'] + // scope: PermissionConfigTable.GET['/'] || DefaultScopes } }, handler: function (request, h) { diff --git a/server/web/routes/signup.js b/server/web/routes/signup.js index 1c535ad..8fcb265 100644 --- a/server/web/routes/signup.js +++ b/server/web/routes/signup.js @@ -20,7 +20,8 @@ const register = function (server, options) { return h.view('signup/signup',{ projectName: Config.get('/projectName'), title: 'Signup', - baseUrl: Config.get('/baseUrl') + baseUrl: Config.get('/baseUrl'), + usernameRequired: Config.get('/loginInfo/usernameRequired') }); } }); diff --git a/server/web/templates/signup/signup.handlebars b/server/web/templates/signup/signup.handlebars index fd3a198..0ed54d9 100644 --- a/server/web/templates/signup/signup.handlebars +++ b/server/web/templates/signup/signup.handlebars @@ -5,7 +5,11 @@

Signup

-
+ {{#if usernameRequired}} +
+ {{else}} +
+ {{/if}}
From d86d06d622c995aef7b3ca142a536d348f372fc3 Mon Sep 17 00:00:00 2001 From: nicholasjhall Date: Thu, 17 Jun 2021 13:59:11 -0400 Subject: [PATCH 2/3] Refactored login with email functionality --- server/api/login.js | 53 ++++++++++++++++----- server/models/auth-attempt.js | 27 +++++------ server/web/public/scripts/login/login.js | 7 ++- server/web/routes/login.js | 6 ++- server/web/templates/login/login.handlebars | 4 ++ 5 files changed, 68 insertions(+), 29 deletions(-) diff --git a/server/api/login.js b/server/api/login.js index 66facd2..a3719ca 100755 --- a/server/api/login.js +++ b/server/api/login.js @@ -15,12 +15,13 @@ const register = function (server, options) { method: 'POST', path: '/api/login', options: { - tags: ['api','auth'], + tags: ['api', 'auth'], description: 'Log in with username and password.', auth: false, validate: { payload: { - username: Joi.string().lowercase().required(), + username: Joi.string().lowercase().optional(), + email: Joi.string().lowercase().optional(), password: Joi.string().required() } }, @@ -34,10 +35,15 @@ const register = function (server, options) { method: async function (request, h) { const ip = request.info.remoteAddress; - const username = request.payload.username; - - const detected = await AuthAttempt.abuseDetected(ip,username); - + let detected; + if (Config.get('/loginInfo/usernameRequired')) { + const username = request.payload.username; + detected = await AuthAttempt.abuseDetected(ip, username); + } + else { + const email = request.payload.email; + detected = await AuthAttempt.abuseDetected(ip, email); + } if (detected) { throw Boom.badRequest('Maximum number of auth attempts reached.'); } @@ -48,15 +54,27 @@ const register = function (server, options) { assign: 'user', method: async function (request, h) { - const username = request.payload.username; + let userInfo; + if (Config.get('/loginInfo/usernameRequired')) { + userInfo = request.payload.username; + if (userInfo.indexOf('@') > -1) { + throw Boom.badRequest('Email cannot be used as username'); + } + } + else { + userInfo = request.payload.email; + if (userInfo.indexOf('@') === -1) { + throw Boom.badRequest('Email required'); + } + } const password = request.payload.password; const ip = request.info.remoteAddress; - const user = await User.findByCredentials(username, password); + const user = await User.findByCredentials(userInfo, password); const userAgent = request.headers['user-agent']; if (!user) { - await AuthAttempt.create(ip, username, userAgent); + await AuthAttempt.create(ip, userInfo, userAgent); throw Boom.badRequest('Credentials are invalid or account is inactive.'); } @@ -87,11 +105,23 @@ const register = function (server, options) { const authHeader = `Basic ${Buffer.from(credentials).toString('base64')}`; request.cookieAuth.set(request.pre.session); + if (Config.get('/loginInfo/usernameRequired')) { + return ({ + user: { + _id: request.pre.user._id, + name: request.pre.user.name, + username: request.pre.user.username, + email: request.pre.user.email, + roles: request.pre.user.roles + }, + session: request.pre.session, + authHeader + }); + } return ({ user: { _id: request.pre.user._id, name: request.pre.user.name, - username: request.pre.user.username, email: request.pre.user.email, roles: request.pre.user.roles }, @@ -102,7 +132,6 @@ const register = function (server, options) { }); - server.route({ method: 'POST', path: '/api/login/forgot', @@ -259,7 +288,7 @@ const register = function (server, options) { method: 'POST', path: '/api/login/reset', options: { - tags: ['api','auth'], + tags: ['api', 'auth'], description: 'Verify Key to reset new password', auth: false, validate: { diff --git a/server/models/auth-attempt.js b/server/models/auth-attempt.js index dfe8a9d..17042cc 100755 --- a/server/models/auth-attempt.js +++ b/server/models/auth-attempt.js @@ -8,10 +8,10 @@ const Hoek = require('hoek'); class AuthAttempt extends AnchorModel { - static async create(ip, username, userAgent) { + static async create(ip, userInfo, userAgent) { Assert.ok(ip, 'Missing ip argument.'); - Assert.ok(username, 'Missing username argument.'); + Assert.ok(userInfo, 'Missing username or email argument.'); Assert.ok(userAgent, 'Missing userAgent argument.'); const agentInfo = UserAgent.lookup(userAgent); @@ -21,7 +21,7 @@ class AuthAttempt extends AnchorModel { browser, ip, os: agentInfo.os.toString(), - username + userInfo }); const authAttempts = await this.insertOne(document); @@ -29,14 +29,14 @@ class AuthAttempt extends AnchorModel { return authAttempts[0]; } - static async abuseDetected(ip, username) { + static async abuseDetected(ip, userInfo) { Assert.ok(ip, 'Missing ip argument.'); - Assert.ok(username, 'Missing username argument.'); + Assert.ok(userInfo, 'Missing username or email argument.'); const [countByIp, countByIpAndUser] = await Promise.all([ this.count({ ip }), - this.count({ ip, username }) + this.count({ ip, userInfo }) ]); const config = Config.get('/authAttempts'); @@ -49,13 +49,12 @@ class AuthAttempt extends AnchorModel { AuthAttempt.collectionName = 'authAttempts'; - AuthAttempt.schema = Joi.object({ _id: Joi.object(), browser: Joi.string(), ip: Joi.string().required(), os: Joi.string().required(), - username: Joi.string().lowercase().required(), + userInfo: Joi.string().lowercase().required(), createdAt: Joi.date().default(new Date(), 'time of creation') }); @@ -64,14 +63,14 @@ AuthAttempt.routes = Hoek.applyToDefaults(AnchorModel.routes, { disabled: true }, update: Joi.object({ - username: Joi.string().lowercase().required() + userInfo: Joi.string().lowercase().required() }), tableView: { outputDataFields: { - username: { label: 'Username' }, + userInfo: { label: 'User Info' }, ip: { label: 'IP' }, createdAt: { label: 'Time' }, - _id: { label: 'ID', accessRoles: ['admin', 'researcher','root'], invisible: true }, + _id: { label: 'ID', accessRoles: ['admin', 'researcher', 'root'], invisible: true }, os: { label: 'OS', invisible: true }, browser: { label: 'browser', invisible: true } @@ -82,7 +81,7 @@ AuthAttempt.routes = Hoek.applyToDefaults(AnchorModel.routes, { }, editView: { editSchema: Joi.object({ - username: Joi.string().lowercase().required() + userInfo: Joi.string().lowercase().required() }) } }); @@ -90,8 +89,8 @@ AuthAttempt.routes = Hoek.applyToDefaults(AnchorModel.routes, { AuthAttempt.lookups = []; AuthAttempt.indexes = [ - { key: { ip: 1, username: 1 } }, - { key: { username: 1 } } + { key: { ip: 1, userInfo: 1 } }, + { key: { userInfo: 1 } } ]; module.exports = AuthAttempt; diff --git a/server/web/public/scripts/login/login.js b/server/web/public/scripts/login/login.js index fc09c98..0326341 100644 --- a/server/web/public/scripts/login/login.js +++ b/server/web/public/scripts/login/login.js @@ -3,7 +3,12 @@ const loginSchema = Joi.object({ username: Joi.string().lowercase().required(), password: Joi.string().required() }); +const loginSchemaEmail = Joi.object({ + email: Joi.string().lowercase().required(), + password: Joi.string().required() +}); joiToForm('loginFormFields',loginSchema); +joiToForm('loginFormFieldsEmail',loginSchemaEmail); $('#login').click((event) => { event.preventDefault(); @@ -16,7 +21,7 @@ $('#login').click((event) => { url: '/api/login', data: values, success: function (result) { - console.log("is success") + console.log("is success") location.reload(); }, error: function (result) { diff --git a/server/web/routes/login.js b/server/web/routes/login.js index d5bcd51..bede86f 100644 --- a/server/web/routes/login.js +++ b/server/web/routes/login.js @@ -9,7 +9,8 @@ const register = function (server, options) { path: '/login', options: { auth: { - strategies: ['session'] + strategies: ['session'], + mode: 'try' } }, handler: function (request, h) { @@ -23,7 +24,8 @@ const register = function (server, options) { return h.view('login/login', { projectName: Config.get('/projectName'), title: 'Login', - baseUrl: Config.get('/baseUrl') + baseUrl: Config.get('/baseUrl'), + usernameRequired: Config.get('/loginInfo/usernameRequired') }); } }); diff --git a/server/web/templates/login/login.handlebars b/server/web/templates/login/login.handlebars index 5b04b60..fc45224 100644 --- a/server/web/templates/login/login.handlebars +++ b/server/web/templates/login/login.handlebars @@ -5,7 +5,11 @@

Login

+ {{#if usernameRequired}}
+ {{else}} +
+ {{/if}}
From 809b93bccb57d9809d772bf8de0ea63ce88895a8 Mon Sep 17 00:00:00 2001 From: nicholasjhall Date: Thu, 17 Jun 2021 15:00:51 -0400 Subject: [PATCH 3/3] Refactor account update --- server/api/users.js | 19 ++++++++++--------- server/web/public/scripts/account.js | 6 ++++++ server/web/routes/account.js | 3 ++- server/web/templates/account/index.handlebars | 4 ++++ 4 files changed, 22 insertions(+), 10 deletions(-) diff --git a/server/api/users.js b/server/api/users.js index 37284c4..6e08195 100755 --- a/server/api/users.js +++ b/server/api/users.js @@ -252,7 +252,7 @@ const register = function (server, options) { }, validate: { payload: { - username: Joi.string().token().lowercase().required(), + username: Joi.string().token().lowercase().optional(), email: Joi.string().email().lowercase().required(), name: Joi.string().required(), gender: Joi.string().allow('male', 'female'), @@ -268,17 +268,18 @@ const register = function (server, options) { assign: 'usernameCheck', method: async function (request, h) { - const conditions = { - username: request.payload.username, - _id: { $ne: request.auth.credentials.user._id } - }; + if (Config.get('/loginInfo/usernameRequired')) { + const conditions = { + username: request.payload.username, + _id: { $ne: request.auth.credentials.user._id } + }; - const user = await User.findOne(conditions); + const user = await User.findOne(conditions); - if (user) { - throw Boom.conflict('Username already in use.'); + if (user) { + throw Boom.conflict('Username already in use.'); + } } - return h.continue; } }, { diff --git a/server/web/public/scripts/account.js b/server/web/public/scripts/account.js index 947a95e..ce8bdc0 100644 --- a/server/web/public/scripts/account.js +++ b/server/web/public/scripts/account.js @@ -3,7 +3,13 @@ const schema = Joi.object({ email: Joi.string().email().lowercase().required(), username: Joi.string().token().lowercase().required(), }); +const schemaNoUsername = Joi.object({ + name: Joi.string().required(), + email: Joi.string().email().lowercase().required() +}); + joiToForm('formFields',schema); +joiToForm('formFieldsNoUsername',schemaNoUsername); $('#update').click((event) => { event.preventDefault(); const values = {}; diff --git a/server/web/routes/account.js b/server/web/routes/account.js index b2fec99..4870bba 100644 --- a/server/web/routes/account.js +++ b/server/web/routes/account.js @@ -17,7 +17,8 @@ const register = function (server, options) { user: request.auth.credentials.user, projectName: Config.get('/projectName'), title: 'Accounts', - baseUrl: Config.get('/baseUrl') + baseUrl: Config.get('/baseUrl'), + usernameRequired: Config.get('/loginInfo/usernameRequired') }); } }); diff --git a/server/web/templates/account/index.handlebars b/server/web/templates/account/index.handlebars index fb1189f..e6ebdca 100755 --- a/server/web/templates/account/index.handlebars +++ b/server/web/templates/account/index.handlebars @@ -28,7 +28,11 @@ {{/if}}

+ {{#if usernameRequired}}
+ {{else}} +
+ {{/if}}