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")'
|
: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 @@
|
|||||||
: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 @@
|
|||||||
: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 @@
|
|||||||
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 @@
|
|||||||
@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 {
|
|||||||
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 @@
|
|||||||
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 @@
|
|||||||
: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 @@
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
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) {
|
|||||||
/**
|
/**
|
||||||
* 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 = {
|
|||||||
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 = {
|
|||||||
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 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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')
|
|||||||
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 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user