Commit 78ae137f authored by Nicolas Giard's avatar Nicolas Giard

feat: register server-side validation + forgot password UI

parent 901dbb98
...@@ -45,7 +45,7 @@ ...@@ -45,7 +45,7 @@
:placeholder='$t("auth:fields.password")' :placeholder='$t("auth:fields.password")'
@keyup.enter='login' @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: .body-2 Enter the security code generated from your trusted device:
v-text-field.md2.centered.mt-2( v-text-field.md2.centered.mt-2(
solo solo
...@@ -57,6 +57,18 @@ ...@@ -57,6 +57,18 @@
:placeholder='$t("auth:tfa.placeholder")' :placeholder='$t("auth:tfa.placeholder")'
@keyup.enter='verifySecurityCode' @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-card-actions.pb-4
v-spacer v-spacer
v-btn.md2( v-btn.md2(
...@@ -69,7 +81,7 @@ ...@@ -69,7 +81,7 @@
:loading='isLoading' :loading='isLoading'
) {{ $t('auth:actions.login') }} ) {{ $t('auth:actions.login') }}
v-btn.md2( v-btn.md2(
v-if='screen === "tfa"' v-else-if='screen === "tfa"'
block block
large large
color='primary' color='primary'
...@@ -77,12 +89,25 @@ ...@@ -77,12 +89,25 @@
round round
:loading='isLoading' :loading='isLoading'
) {{ $t('auth:tfa.verifyToken') }} ) {{ $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-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 v-spacer
a.caption(href='') {{ $t('auth:forgotPasswordLink') }} a.caption(@click.stop.prevent='forgotPassword', href='#forgot') {{ $t('auth:forgotPasswordLink') }}
v-spacer 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-divider
v-card-text.grey.lighten-4.text-xs-center v-card-text.grey.lighten-4.text-xs-center
.pb-2.body-2.text-xs-center.grey--text.text--darken-2 {{ $t('auth:orLoginUsingStrategy') }} .pb-2.body-2.text-xs-center.grey--text.text--darken-2 {{ $t('auth:orLoginUsingStrategy') }}
...@@ -95,7 +120,7 @@ ...@@ -95,7 +120,7 @@
@click='selectStrategy(strategy)' @click='selectStrategy(strategy)'
) )
span {{ strategy.title }} span {{ strategy.title }}
template(v-if='selectedStrategy.selfRegistration') template(v-if='screen === "login" && selectedStrategy.selfRegistration')
v-divider v-divider
v-card-actions.py-3(:class='isSocialShown ? "" : "grey lighten-4"') v-card-actions.py-3(:class='isSocialShown ? "" : "grey lighten-4"')
v-spacer v-spacer
...@@ -286,6 +311,19 @@ export default { ...@@ -286,6 +311,19 @@ export default {
this.isLoading = false 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: { apollo: {
......
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
offset-lg3, lg6 offset-lg3, lg6
offset-xl4, xl4 offset-xl4, xl4
) )
transition(name='zoom') transition(name='fadeUp')
v-card.elevation-5.md2(v-show='isShown') v-card.elevation-5.md2(v-show='isShown')
v-toolbar(color='indigo', flat, dense, dark) v-toolbar(color='indigo', flat, dense, dark)
v-spacer v-spacer
...@@ -43,6 +43,7 @@ ...@@ -43,6 +43,7 @@
:placeholder='$t("auth:fields.password")' :placeholder='$t("auth:fields.password")'
color='indigo' color='indigo'
loading loading
counter='255'
) )
password-strength(slot='progress', v-model='password') password-strength(slot='progress', v-model='password')
v-text-field.md2.mt-2( v-text-field.md2.mt-2(
...@@ -63,12 +64,12 @@ ...@@ -63,12 +64,12 @@
flat flat
prepend-icon='person' prepend-icon='person'
background-color='grey lighten-4' background-color='grey lighten-4'
hide-details
ref='iptName' ref='iptName'
v-model='name' v-model='name'
:placeholder='$t("auth:fields.name")' :placeholder='$t("auth:fields.name")'
@keyup.enter='register' @keyup.enter='register'
color='indigo' color='indigo'
counter='255'
) )
v-card-actions.pb-4 v-card-actions.pb-4
v-spacer v-spacer
...@@ -116,7 +117,9 @@ export default { ...@@ -116,7 +117,9 @@ export default {
name: '', name: '',
hidePassword: true, hidePassword: true,
isLoading: false, isLoading: false,
isShown: false isShown: false,
loaderColor: 'grey darken-4',
loaderTitle: 'Working...'
} }
}, },
computed: { computed: {
...@@ -211,6 +214,8 @@ export default { ...@@ -211,6 +214,8 @@ export default {
this.$refs.iptName.focus() this.$refs.iptName.focus()
} }
} else { } else {
this.loaderColor = 'grey darken-4'
this.loaderTitle = this.$t('auth:registering')
this.isLoading = true this.isLoading = true
try { try {
let resp = await this.$apollo.mutate({ let resp = await this.$apollo.mutate({
...@@ -224,11 +229,8 @@ export default { ...@@ -224,11 +229,8 @@ export default {
if (_.has(resp, 'data.authentication.register')) { if (_.has(resp, 'data.authentication.register')) {
let respObj = _.get(resp, 'data.authentication.register', {}) let respObj = _.get(resp, 'data.authentication.register', {})
if (respObj.responseResult.succeeded === true) { if (respObj.responseResult.succeeded === true) {
this.$store.commit('showNotification', { this.loaderColor = 'green'
message: 'Account created successfully! Redirecting...', this.loaderTitle = this.$t('auth:registerSuccess')
style: 'success',
icon: 'check'
})
Cookies.set('jwt', respObj.jwt, { expires: 365 }) Cookies.set('jwt', respObj.jwt, { expires: 365 })
_.delay(() => { _.delay(() => {
window.location.replace('/') window.location.replace('/')
...@@ -237,7 +239,7 @@ export default { ...@@ -237,7 +239,7 @@ export default {
throw new Error(respObj.responseResult.message) throw new Error(respObj.responseResult.message)
} }
} else { } else {
throw new Error('Registration is unavailable at this time.') throw new Error(this.$t('auth:genericError'))
} }
} catch (err) { } catch (err) {
console.error(err) console.error(err)
......
...@@ -21,8 +21,13 @@ router.get('/logout', function (req, res) { ...@@ -21,8 +21,13 @@ router.get('/logout', function (req, res) {
/** /**
* Register form * Register form
*/ */
router.get('/register', function (req, res, next) { router.get('/register', async (req, res, next) => {
res.render('register') const localStrg = await WIKI.models.authentication.getStrategy('local')
if (localStrg.selfRegistration) {
res.render('register')
} else {
next(new WIKI.Error.AuthRegistrationDisabled())
}
}) })
/** /**
......
...@@ -17,6 +17,14 @@ module.exports = { ...@@ -17,6 +17,14 @@ module.exports = {
message: 'An account already exists using this email address.', message: 'An account already exists using this email address.',
code: 1004 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', { AuthTFAFailed: CustomError('AuthTFAFailed', {
message: 'Incorrect TFA Security Code.', message: 'Incorrect TFA Security Code.',
code: 1005 code: 1005
...@@ -33,6 +41,10 @@ module.exports = { ...@@ -33,6 +41,10 @@ module.exports = {
message: 'Too many attempts! Try again later.', message: 'Too many attempts! Try again later.',
code: 1008 code: 1008
}), }),
InputInvalid: CustomError('InputInvalid', {
message: 'Input data is invalid.',
code: 1013
}),
LocaleInvalidNamespace: CustomError('LocaleInvalidNamespace', { LocaleInvalidNamespace: CustomError('LocaleInvalidNamespace', {
message: 'Invalid locale or namespace.', message: 'Invalid locale or namespace.',
code: 1009 code: 1009
......
...@@ -30,6 +30,10 @@ module.exports = class Authentication extends Model { ...@@ -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) { static async getStrategies(isEnabled) {
const strategies = await WIKI.models.authentication.query().where(_.isBoolean(isEnabled) ? { isEnabled } : {}) const strategies = await WIKI.models.authentication.query().where(_.isBoolean(isEnabled) ? { isEnabled } : {})
return _.sortBy(strategies.map(str => ({ return _.sortBy(strategies.map(str => ({
......
...@@ -6,6 +6,7 @@ const tfa = require('node-2fa') ...@@ -6,6 +6,7 @@ const tfa = require('node-2fa')
const securityHelper = require('../helpers/security') const securityHelper = require('../helpers/security')
const jwt = require('jsonwebtoken') const jwt = require('jsonwebtoken')
const Model = require('objection').Model const Model = require('objection').Model
const validate = require('validate.js')
const bcryptRegexp = /^\$2[ayb]\$[0-9]{2}\$[A-Za-z0-9./]{53}$/ const bcryptRegexp = /^\$2[ayb]\$[0-9]{2}\$[A-Za-z0-9./]{53}$/
...@@ -294,21 +295,70 @@ module.exports = class User extends Model { ...@@ -294,21 +295,70 @@ module.exports = class User extends Model {
} }
static async register ({ email, password, name }, context) { static async register ({ email, password, name }, context) {
const usr = await WIKI.models.users.query().findOne({ email, providerKey: 'local' }) const localStrg = await WIKI.models.authentication.getStrategy('local')
if (!usr) { // Check if self-registration is enabled
await WIKI.models.users.query().insert({ if (localStrg.selfRegistration) {
provider: 'local', // Input validation
const validation = validate({
email, email,
name,
password, password,
locale: 'en', name
defaultEditor: 'markdown', }, {
tfaIsActive: false, email: {
isSystem: false email: true,
}) length: {
return true 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 { } else {
throw new WIKI.Error.AuthAccountAlreadyExists() throw new WIKI.Error.AuthRegistrationDisabled()
} }
} }
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment