mirror of https://github.com/requarks/wiki
parent
17244a0cb3
commit
901dbb98e0
@ -0,0 +1,51 @@
|
|||||||
|
<template lang='pug'>
|
||||||
|
v-dialog(v-model='value', persistent, max-width='350')
|
||||||
|
v-card.loader-dialog.radius-7(:color='color', dark)
|
||||||
|
v-card-text.text-xs-center.py-4
|
||||||
|
atom-spinner.is-inline(
|
||||||
|
:animation-duration='1000'
|
||||||
|
:size='60'
|
||||||
|
color='#FFF'
|
||||||
|
)
|
||||||
|
.subheading {{ title }}
|
||||||
|
.caption {{ subtitle }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { AtomSpinner } from 'epic-spinners'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
AtomSpinner
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
type: String,
|
||||||
|
default: 'blue darken-3'
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: 'Working...'
|
||||||
|
},
|
||||||
|
subtitle: {
|
||||||
|
type: String,
|
||||||
|
default: 'Please wait'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang='scss'>
|
||||||
|
.loader-dialog {
|
||||||
|
.atom-spinner.is-inline {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.caption {
|
||||||
|
color: rgba(255,255,255,.7);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,79 @@
|
|||||||
|
<template lang="pug">
|
||||||
|
.password-strength
|
||||||
|
v-progress-linear(
|
||||||
|
:color='passwordStrengthColor'
|
||||||
|
v-model='passwordStrength'
|
||||||
|
height='2'
|
||||||
|
)
|
||||||
|
.caption(v-if='!hideText', :class='passwordStrengthColor + "--text"') {{passwordStrengthText}}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import zxcvbn from 'zxcvbn'
|
||||||
|
import _ from 'lodash'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
hideText: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
passwordStrength: 0,
|
||||||
|
passwordStrengthColor: 'grey',
|
||||||
|
passwordStrengthText: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
value(newValue) {
|
||||||
|
this.checkPasswordStrength(newValue)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
checkPasswordStrength: _.debounce(function (pwd) {
|
||||||
|
if (!pwd || pwd.length < 1) {
|
||||||
|
this.passwordStrength = 0
|
||||||
|
this.passwordStrengthColor = 'grey'
|
||||||
|
this.passwordStrengthText = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const strength = zxcvbn(pwd)
|
||||||
|
this.passwordStrength = _.round((strength.score + 1 ) / 5 * 100)
|
||||||
|
if (this.passwordStrength <= 20) {
|
||||||
|
this.passwordStrengthColor = 'red'
|
||||||
|
this.passwordStrengthText = 'Very Weak'
|
||||||
|
} else if (this.passwordStrength <= 40) {
|
||||||
|
this.passwordStrengthColor = 'orange'
|
||||||
|
this.passwordStrengthText = 'Weak'
|
||||||
|
} else if (this.passwordStrength <= 60) {
|
||||||
|
this.passwordStrengthColor = 'teal'
|
||||||
|
this.passwordStrengthText = 'Average'
|
||||||
|
} else if (this.passwordStrength <= 80) {
|
||||||
|
this.passwordStrengthColor = 'green'
|
||||||
|
this.passwordStrengthText = 'Strong'
|
||||||
|
} else {
|
||||||
|
this.passwordStrengthColor = 'green'
|
||||||
|
this.passwordStrengthText = 'Very Strong'
|
||||||
|
}
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
|
||||||
|
.password-strength > .caption {
|
||||||
|
width: 100%;
|
||||||
|
left: 0;
|
||||||
|
margin: 0;
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
@ -0,0 +1,303 @@
|
|||||||
|
<template lang="pug">
|
||||||
|
v-app
|
||||||
|
.register
|
||||||
|
v-container(grid-list-lg)
|
||||||
|
v-layout(row, wrap)
|
||||||
|
v-flex(
|
||||||
|
xs12
|
||||||
|
offset-sm1, sm10
|
||||||
|
offset-md2, md8
|
||||||
|
offset-lg3, lg6
|
||||||
|
offset-xl4, xl4
|
||||||
|
)
|
||||||
|
transition(name='zoom')
|
||||||
|
v-card.elevation-5.md2(v-show='isShown')
|
||||||
|
v-toolbar(color='indigo', flat, dense, dark)
|
||||||
|
v-spacer
|
||||||
|
.subheading {{ $t('auth:registerTitle') }}
|
||||||
|
v-spacer
|
||||||
|
v-card-text.text-xs-center
|
||||||
|
h1.display-1.indigo--text.py-2 {{ siteTitle }}
|
||||||
|
.body-2 {{ $t('auth:registerSubTitle') }}
|
||||||
|
v-text-field.md2.mt-3(
|
||||||
|
solo
|
||||||
|
flat
|
||||||
|
prepend-icon='email'
|
||||||
|
background-color='grey lighten-4'
|
||||||
|
hide-details
|
||||||
|
ref='iptEmail'
|
||||||
|
v-model='email'
|
||||||
|
:placeholder='$t("auth:fields.email")'
|
||||||
|
color='indigo'
|
||||||
|
)
|
||||||
|
v-text-field.md2.mt-2(
|
||||||
|
solo
|
||||||
|
flat
|
||||||
|
prepend-icon='vpn_key'
|
||||||
|
background-color='grey lighten-4'
|
||||||
|
ref='iptPassword'
|
||||||
|
v-model='password'
|
||||||
|
:append-icon='hidePassword ? "visibility" : "visibility_off"'
|
||||||
|
@click:append='() => (hidePassword = !hidePassword)'
|
||||||
|
:type='hidePassword ? "password" : "text"'
|
||||||
|
:placeholder='$t("auth:fields.password")'
|
||||||
|
color='indigo'
|
||||||
|
loading
|
||||||
|
)
|
||||||
|
password-strength(slot='progress', v-model='password')
|
||||||
|
v-text-field.md2.mt-2(
|
||||||
|
solo
|
||||||
|
flat
|
||||||
|
prepend-icon='vpn_key'
|
||||||
|
background-color='grey lighten-4'
|
||||||
|
hide-details
|
||||||
|
ref='iptVerifyPassword'
|
||||||
|
v-model='verifyPassword'
|
||||||
|
@click:append='() => (hidePassword = !hidePassword)'
|
||||||
|
type='password'
|
||||||
|
:placeholder='$t("auth:fields.verifyPassword")'
|
||||||
|
color='indigo'
|
||||||
|
)
|
||||||
|
v-text-field.md2.mt-2(
|
||||||
|
solo
|
||||||
|
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'
|
||||||
|
)
|
||||||
|
v-card-actions.pb-4
|
||||||
|
v-spacer
|
||||||
|
v-btn.md2(
|
||||||
|
block
|
||||||
|
large
|
||||||
|
dark
|
||||||
|
color='indigo'
|
||||||
|
@click='register'
|
||||||
|
round
|
||||||
|
:loading='isLoading'
|
||||||
|
) {{ $t('auth:actions.register') }}
|
||||||
|
v-spacer
|
||||||
|
v-divider
|
||||||
|
v-card-actions.py-3.grey.lighten-4
|
||||||
|
v-spacer
|
||||||
|
i18next.caption(path='auth:switchToLogin.text', tag='div')
|
||||||
|
a.caption(href='/login', place='link') {{ $t('auth:switchToLogin.link') }}
|
||||||
|
v-spacer
|
||||||
|
|
||||||
|
loader(v-model='isLoading', :color='loaderColor', :title='loaderTitle', :subtitle='$t(`auth:pleaseWait`)')
|
||||||
|
nav-footer(color='grey darken-4', dark-color='grey darken-4')
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
/* global siteConfig */
|
||||||
|
|
||||||
|
import _ from 'lodash'
|
||||||
|
import Cookies from 'js-cookie'
|
||||||
|
import validate from 'validate.js'
|
||||||
|
import PasswordStrength from './common/password-strength.vue'
|
||||||
|
|
||||||
|
import registerMutation from 'gql/register/register-mutation-create.gql'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
i18nOptions: { namespaces: 'auth' },
|
||||||
|
components: {
|
||||||
|
PasswordStrength
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
verifyPassword: '',
|
||||||
|
name: '',
|
||||||
|
hidePassword: true,
|
||||||
|
isLoading: false,
|
||||||
|
isShown: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
siteTitle () {
|
||||||
|
return siteConfig.title
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
this.isShown = true
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.iptEmail.focus()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
/**
|
||||||
|
* REGISTER
|
||||||
|
*/
|
||||||
|
async register () {
|
||||||
|
const validation = validate({
|
||||||
|
email: this.email,
|
||||||
|
password: this.password,
|
||||||
|
verifyPassword: this.verifyPassword,
|
||||||
|
name: this.name
|
||||||
|
}, {
|
||||||
|
email: {
|
||||||
|
presence: {
|
||||||
|
message: this.$t('auth:missingEmail'),
|
||||||
|
allowEmpty: false
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
message: this.$t('auth:invalidEmail')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
password: {
|
||||||
|
presence: {
|
||||||
|
message: this.$t('auth:missingPassword'),
|
||||||
|
allowEmpty: false
|
||||||
|
},
|
||||||
|
length: {
|
||||||
|
minimum: 6,
|
||||||
|
tooShort: this.$t('auth:passwordTooShort')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
verifyPassword: {
|
||||||
|
equality: {
|
||||||
|
attribute: 'password',
|
||||||
|
message: this.$t('auth:passwordNotMatch')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
presence: {
|
||||||
|
message: this.$t('auth:missingName'),
|
||||||
|
allowEmpty: false
|
||||||
|
},
|
||||||
|
length: {
|
||||||
|
minimum: 2,
|
||||||
|
maximum: 255,
|
||||||
|
tooShort: this.$t('auth:nameTooShort'),
|
||||||
|
tooLong: this.$t('auth:nameTooLong')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}, { fullMessages: false })
|
||||||
|
|
||||||
|
if (validation) {
|
||||||
|
if(validation.email) {
|
||||||
|
this.$store.commit('showNotification', {
|
||||||
|
style: 'red',
|
||||||
|
message: validation.email[0],
|
||||||
|
icon: 'warning'
|
||||||
|
})
|
||||||
|
this.$refs.iptEmail.focus()
|
||||||
|
} else if (validation.password) {
|
||||||
|
this.$store.commit('showNotification', {
|
||||||
|
style: 'red',
|
||||||
|
message: validation.password[0],
|
||||||
|
icon: 'warning'
|
||||||
|
})
|
||||||
|
this.$refs.iptPassword.focus()
|
||||||
|
} else if (validation.verifyPassword) {
|
||||||
|
this.$store.commit('showNotification', {
|
||||||
|
style: 'red',
|
||||||
|
message: validation.verifyPassword[0],
|
||||||
|
icon: 'warning'
|
||||||
|
})
|
||||||
|
this.$refs.iptVerifyPassword.focus()
|
||||||
|
} else {
|
||||||
|
this.$store.commit('showNotification', {
|
||||||
|
style: 'red',
|
||||||
|
message: validation.name[0],
|
||||||
|
icon: 'warning'
|
||||||
|
})
|
||||||
|
this.$refs.iptName.focus()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.isLoading = true
|
||||||
|
try {
|
||||||
|
let resp = await this.$apollo.mutate({
|
||||||
|
mutation: registerMutation,
|
||||||
|
variables: {
|
||||||
|
email: this.email,
|
||||||
|
password: this.password,
|
||||||
|
name: this.name
|
||||||
|
}
|
||||||
|
})
|
||||||
|
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'
|
||||||
|
})
|
||||||
|
Cookies.set('jwt', respObj.jwt, { expires: 365 })
|
||||||
|
_.delay(() => {
|
||||||
|
window.location.replace('/')
|
||||||
|
}, 1000)
|
||||||
|
} else {
|
||||||
|
throw new Error(respObj.responseResult.message)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error('Registration is unavailable at this time.')
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
this.$store.commit('showNotification', {
|
||||||
|
style: 'red',
|
||||||
|
message: err.message,
|
||||||
|
icon: 'warning'
|
||||||
|
})
|
||||||
|
this.isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.register {
|
||||||
|
background-color: mc('indigo', '900');
|
||||||
|
background-image: url('../static/svg/motif-blocks.svg');
|
||||||
|
background-repeat: repeat;
|
||||||
|
background-size: 200px;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
animation: loginBgReveal 20s linear infinite;
|
||||||
|
|
||||||
|
@include keyframes(loginBgReveal) {
|
||||||
|
0% {
|
||||||
|
background-position-x: 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position-x: 800px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
background-image: url('../static/svg/motif-overlay.svg');
|
||||||
|
background-attachment: fixed;
|
||||||
|
background-size: cover;
|
||||||
|
opacity: .5;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .container {
|
||||||
|
height: 100%;
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-family: 'Varela Round' !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-text-field.centered input {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,13 @@
|
|||||||
|
mutation($email: String!, $password: String!, $name: String!) {
|
||||||
|
authentication {
|
||||||
|
register(email: $email, password: $password, name: $name) {
|
||||||
|
responseResult {
|
||||||
|
succeeded
|
||||||
|
errorCode
|
||||||
|
slug
|
||||||
|
message
|
||||||
|
}
|
||||||
|
jwt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Before Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 4.9 KiB |
Before Width: | Height: | Size: 4.2 KiB |
After Width: | Height: | Size: 4.1 KiB |
@ -1,30 +1,44 @@
|
|||||||
class BaseError extends Error {
|
const CustomError = require('custom-error-instance')
|
||||||
constructor (message) {
|
|
||||||
super(message)
|
|
||||||
this.name = this.constructor.name
|
|
||||||
Error.captureStackTrace(this, this.constructor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class AuthGenericError extends BaseError { constructor (message = 'An unexpected error occured during login.') { super(message) } }
|
|
||||||
class AuthLoginFailed extends BaseError { constructor (message = 'Invalid email / username or password.') { super(message) } }
|
|
||||||
class AuthProviderInvalid extends BaseError { constructor (message = 'Invalid authentication provider.') { super(message) } }
|
|
||||||
class AuthTFAFailed extends BaseError { constructor (message = 'Incorrect TFA Security Code.') { super(message) } }
|
|
||||||
class AuthTFAInvalid extends BaseError { constructor (message = 'Invalid TFA Security Code or Login Token.') { super(message) } }
|
|
||||||
class BruteInstanceIsInvalid extends BaseError { constructor (message = 'Invalid Brute Force Instance.') { super(message) } }
|
|
||||||
class BruteTooManyAttempts extends BaseError { constructor (message = 'Too many attempts! Try again later.') { super(message) } }
|
|
||||||
class LocaleInvalidNamespace extends BaseError { constructor (message = 'Invalid locale or namespace.') { super(message) } }
|
|
||||||
class UserCreationFailed extends BaseError { constructor (message = 'An unexpected error occured during user creation.') { super(message) } }
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
BaseError,
|
AuthGenericError: CustomError('AuthGenericError', {
|
||||||
AuthGenericError,
|
message: 'An unexpected error occured during login.',
|
||||||
AuthLoginFailed,
|
code: 1001
|
||||||
AuthProviderInvalid,
|
}),
|
||||||
AuthTFAFailed,
|
AuthLoginFailed: CustomError('AuthLoginFailed', {
|
||||||
AuthTFAInvalid,
|
message: 'Invalid email / username or password.',
|
||||||
BruteInstanceIsInvalid,
|
code: 1002
|
||||||
BruteTooManyAttempts,
|
}),
|
||||||
LocaleInvalidNamespace,
|
AuthProviderInvalid: CustomError('AuthProviderInvalid', {
|
||||||
UserCreationFailed
|
message: 'Invalid authentication provider.',
|
||||||
|
code: 1003
|
||||||
|
}),
|
||||||
|
AuthAccountAlreadyExists: CustomError('AuthAccountAlreadyExists', {
|
||||||
|
message: 'An account already exists using this email address.',
|
||||||
|
code: 1004
|
||||||
|
}),
|
||||||
|
AuthTFAFailed: CustomError('AuthTFAFailed', {
|
||||||
|
message: 'Incorrect TFA Security Code.',
|
||||||
|
code: 1005
|
||||||
|
}),
|
||||||
|
AuthTFAInvalid: CustomError('AuthTFAInvalid', {
|
||||||
|
message: 'Invalid TFA Security Code or Login Token.',
|
||||||
|
code: 1006
|
||||||
|
}),
|
||||||
|
BruteInstanceIsInvalid: CustomError('BruteInstanceIsInvalid', {
|
||||||
|
message: 'Invalid Brute Force Instance.',
|
||||||
|
code: 1007
|
||||||
|
}),
|
||||||
|
BruteTooManyAttempts: CustomError('BruteTooManyAttempts', {
|
||||||
|
message: 'Too many attempts! Try again later.',
|
||||||
|
code: 1008
|
||||||
|
}),
|
||||||
|
LocaleInvalidNamespace: CustomError('LocaleInvalidNamespace', {
|
||||||
|
message: 'Invalid locale or namespace.',
|
||||||
|
code: 1009
|
||||||
|
}),
|
||||||
|
UserCreationFailed: CustomError('UserCreationFailed', {
|
||||||
|
message: 'An unexpected error occured during user creation.',
|
||||||
|
code: 1010
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
extends master.pug
|
||||||
|
|
||||||
|
block body
|
||||||
|
#root.is-fullscreen
|
||||||
|
register
|
Loading…
Reference in new issue