feat: register server-side validation + forgot password UI
This commit is contained in:
parent
901dbb98e0
commit
78ae137f48
@ -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: {
|
||||
|
@ -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)
|
||||
|
@ -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())
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
|
@ -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 => ({
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user