diff --git a/client/components/login.vue b/client/components/login.vue index 24ffa9a3..964ab293 100644 --- a/client/components/login.vue +++ b/client/components/login.vue @@ -45,7 +45,7 @@ :placeholder='$t("auth:fields.password")' @keyup.enter='login' ) - template(v-if='screen === "tfa"') + template(v-else-if='screen === "tfa"') .body-2 Enter the security code generated from your trusted device: v-text-field.md2.centered.mt-2( solo @@ -57,6 +57,18 @@ :placeholder='$t("auth:tfa.placeholder")' @keyup.enter='verifySecurityCode' ) + template(v-else-if='screen === "forgot"') + .body-2 {{ $t('auth:forgotPasswordSubtitle') }} + v-text-field.md2.mt-3( + solo + flat + prepend-icon='email' + background-color='grey lighten-4' + hide-details + ref='iptEmailForgot' + v-model='username' + :placeholder='$t("auth:fields.email")' + ) v-card-actions.pb-4 v-spacer v-btn.md2( @@ -69,7 +81,7 @@ :loading='isLoading' ) {{ $t('auth:actions.login') }} v-btn.md2( - v-if='screen === "tfa"' + v-else-if='screen === "tfa"' block large color='primary' @@ -77,12 +89,25 @@ round :loading='isLoading' ) {{ $t('auth:tfa.verifyToken') }} + v-btn.md2( + v-else-if='screen === "forgot"' + block + large + color='primary' + @click='forgotPasswordSubmit' + round + :loading='isLoading' + ) {{ $t('auth:sendResetPassword') }} v-spacer - v-card-actions.pb-3(v-if='selectedStrategy.key === "local"') + v-card-actions.pb-3(v-if='screen === "login" && selectedStrategy.key === "local"') v-spacer - a.caption(href='') {{ $t('auth:forgotPasswordLink') }} + a.caption(@click.stop.prevent='forgotPassword', href='#forgot') {{ $t('auth:forgotPasswordLink') }} v-spacer - template(v-if='isSocialShown') + v-card-actions.pb-3(v-else-if='screen === "forgot"') + v-spacer + a.caption(@click.stop.prevent='screen = `login`', href='#cancelforgot') {{ $t('auth:forgotPasswordCancel') }} + v-spacer + template(v-if='screen === "login" && isSocialShown') v-divider v-card-text.grey.lighten-4.text-xs-center .pb-2.body-2.text-xs-center.grey--text.text--darken-2 {{ $t('auth:orLoginUsingStrategy') }} @@ -95,7 +120,7 @@ @click='selectStrategy(strategy)' ) span {{ strategy.title }} - template(v-if='selectedStrategy.selfRegistration') + template(v-if='screen === "login" && selectedStrategy.selfRegistration') v-divider v-card-actions.py-3(:class='isSocialShown ? "" : "grey lighten-4"') v-spacer @@ -286,6 +311,19 @@ export default { this.isLoading = false }) } + }, + forgotPassword() { + this.screen = 'forgot' + this.$nextTick(() => { + this.$refs.iptEmailForgot.focus() + }) + }, + async forgotPasswordSubmit() { + this.$store.commit('showNotification', { + style: 'pink', + message: 'Coming soon!', + icon: 'free_breakfast' + }) } }, apollo: { diff --git a/client/components/register.vue b/client/components/register.vue index d253220c..36181386 100644 --- a/client/components/register.vue +++ b/client/components/register.vue @@ -10,7 +10,7 @@ offset-lg3, lg6 offset-xl4, xl4 ) - transition(name='zoom') + transition(name='fadeUp') v-card.elevation-5.md2(v-show='isShown') v-toolbar(color='indigo', flat, dense, dark) v-spacer @@ -43,6 +43,7 @@ :placeholder='$t("auth:fields.password")' color='indigo' loading + counter='255' ) password-strength(slot='progress', v-model='password') v-text-field.md2.mt-2( @@ -63,12 +64,12 @@ flat prepend-icon='person' background-color='grey lighten-4' - hide-details ref='iptName' v-model='name' :placeholder='$t("auth:fields.name")' @keyup.enter='register' color='indigo' + counter='255' ) v-card-actions.pb-4 v-spacer @@ -116,7 +117,9 @@ export default { name: '', hidePassword: true, isLoading: false, - isShown: false + isShown: false, + loaderColor: 'grey darken-4', + loaderTitle: 'Working...' } }, computed: { @@ -211,6 +214,8 @@ export default { this.$refs.iptName.focus() } } else { + this.loaderColor = 'grey darken-4' + this.loaderTitle = this.$t('auth:registering') this.isLoading = true try { let resp = await this.$apollo.mutate({ @@ -224,11 +229,8 @@ export default { if (_.has(resp, 'data.authentication.register')) { let respObj = _.get(resp, 'data.authentication.register', {}) if (respObj.responseResult.succeeded === true) { - this.$store.commit('showNotification', { - message: 'Account created successfully! Redirecting...', - style: 'success', - icon: 'check' - }) + this.loaderColor = 'green' + this.loaderTitle = this.$t('auth:registerSuccess') Cookies.set('jwt', respObj.jwt, { expires: 365 }) _.delay(() => { window.location.replace('/') @@ -237,7 +239,7 @@ export default { throw new Error(respObj.responseResult.message) } } else { - throw new Error('Registration is unavailable at this time.') + throw new Error(this.$t('auth:genericError')) } } catch (err) { console.error(err) diff --git a/server/controllers/auth.js b/server/controllers/auth.js index fe20b107..5f111a10 100644 --- a/server/controllers/auth.js +++ b/server/controllers/auth.js @@ -21,8 +21,13 @@ router.get('/logout', function (req, res) { /** * Register form */ -router.get('/register', function (req, res, next) { - res.render('register') +router.get('/register', async (req, res, next) => { + const localStrg = await WIKI.models.authentication.getStrategy('local') + if (localStrg.selfRegistration) { + res.render('register') + } else { + next(new WIKI.Error.AuthRegistrationDisabled()) + } }) /** diff --git a/server/helpers/error.js b/server/helpers/error.js index 3643372b..345aed58 100644 --- a/server/helpers/error.js +++ b/server/helpers/error.js @@ -17,6 +17,14 @@ module.exports = { message: 'An account already exists using this email address.', code: 1004 }), + AuthRegistrationDisabled: CustomError('AuthRegistrationDisabled', { + message: 'Registration is disabled. Contact your system administrator.', + code: 1011 + }), + AuthRegistrationDomainUnauthorized: CustomError('AuthRegistrationDomainUnauthorized', { + message: 'You are not authorized to register. Must use a whitelisted domain.', + code: 1012 + }), AuthTFAFailed: CustomError('AuthTFAFailed', { message: 'Incorrect TFA Security Code.', code: 1005 @@ -33,6 +41,10 @@ module.exports = { message: 'Too many attempts! Try again later.', code: 1008 }), + InputInvalid: CustomError('InputInvalid', { + message: 'Input data is invalid.', + code: 1013 + }), LocaleInvalidNamespace: CustomError('LocaleInvalidNamespace', { message: 'Invalid locale or namespace.', code: 1009 diff --git a/server/models/authentication.js b/server/models/authentication.js index 44bc913d..44273ff0 100644 --- a/server/models/authentication.js +++ b/server/models/authentication.js @@ -30,6 +30,10 @@ module.exports = class Authentication extends Model { } } + static async getStrategy(key) { + return WIKI.models.authentication.query().findOne({ key }) + } + static async getStrategies(isEnabled) { const strategies = await WIKI.models.authentication.query().where(_.isBoolean(isEnabled) ? { isEnabled } : {}) return _.sortBy(strategies.map(str => ({ diff --git a/server/models/users.js b/server/models/users.js index 28e24c1e..2174d75e 100644 --- a/server/models/users.js +++ b/server/models/users.js @@ -6,6 +6,7 @@ const tfa = require('node-2fa') const securityHelper = require('../helpers/security') const jwt = require('jsonwebtoken') const Model = require('objection').Model +const validate = require('validate.js') const bcryptRegexp = /^\$2[ayb]\$[0-9]{2}\$[A-Za-z0-9./]{53}$/ @@ -294,21 +295,70 @@ module.exports = class User extends Model { } static async register ({ email, password, name }, context) { - const usr = await WIKI.models.users.query().findOne({ email, providerKey: 'local' }) - if (!usr) { - await WIKI.models.users.query().insert({ - provider: 'local', + const localStrg = await WIKI.models.authentication.getStrategy('local') + // Check if self-registration is enabled + if (localStrg.selfRegistration) { + // Input validation + const validation = validate({ email, - name, password, - locale: 'en', - defaultEditor: 'markdown', - tfaIsActive: false, - isSystem: false - }) - return true + name + }, { + email: { + email: true, + length: { + maximum: 255 + } + }, + password: { + presence: { + allowEmpty: false + }, + length: { + minimum: 6 + } + }, + name: { + presence: { + allowEmpty: false + }, + length: { + minimum: 2, + maximum: 255 + } + }, + }, { format: 'flat' }) + if (validation && validation.length > 0) { + throw new WIKI.Error.InputInvalid(validation[0]) + } + + // Check if email domain is whitelisted + if (_.get(localStrg, 'domainWhitelist.v', []).length > 0) { + const emailDomain = _.last(email.split('@')) + if (!_.includes(localStrg.domainWhitelist.v, emailDomain)) { + throw new WIKI.Error.AuthRegistrationDomainUnauthorized() + } + } + // Check if email already exists + const usr = await WIKI.models.users.query().findOne({ email, providerKey: 'local' }) + if (!usr) { + // Create the account + await WIKI.models.users.query().insert({ + provider: 'local', + email, + name, + password, + locale: 'en', + defaultEditor: 'markdown', + tfaIsActive: false, + isSystem: false + }) + return true + } else { + throw new WIKI.Error.AuthAccountAlreadyExists() + } } else { - throw new WIKI.Error.AuthAccountAlreadyExists() + throw new WIKI.Error.AuthRegistrationDisabled() } } }