feat: login + TFA authentication

This commit is contained in:
NGPixel
2018-01-09 20:41:53 -05:00
parent 85717bd369
commit cb0d86906f
14 changed files with 402 additions and 44 deletions

View File

@@ -1,20 +1,29 @@
<template lang="pug">
.login(:class='{ "is-error": error }')
.login-container(:class='{ "is-expanded": strategies.length > 1 }')
.login-container(:class='{ "is-expanded": strategies.length > 1, "is-loading": isLoading }')
.login-providers(v-show='strategies.length > 1')
button(v-for='strategy in strategies', :class='{ "is-active": strategy.key === selectedStrategy }', @click='selectStrategy(strategy.key, strategy.useForm)', :title='strategy.title')
em(v-html='strategy.icon')
span {{ strategy.title }}
.login-providers-fill
.login-frame
.login-frame(v-show='screen === "login"')
h1 {{ siteTitle }}
h2 {{ $t('auth:loginrequired') }}
input(type='text', ref='iptEmail', :placeholder='$t("auth:fields.emailuser")')
input(type='password', ref='iptPassword', :placeholder='$t("auth:fields.password")')
h2 {{ $t('auth:loginRequired') }}
input(type='text', ref='iptEmail', v-model='username', :placeholder='$t("auth:fields.emailUser")')
input(type='password', ref='iptPassword', v-model='password', :placeholder='$t("auth:fields.password")', @keyup.enter='login')
button.button.is-blue.is-fullwidth(@click='login')
span {{ $t('auth:actions.login') }}
.login-frame(v-show='screen === "tfa"')
.login-frame-icon
svg.icons.is-48(role='img')
title {{ $t('auth:tfa.title') }}
use(xlink:href='#nc-key')
h2 {{ $t('auth:tfa.subtitle') }}
input(type='text', ref='iptTFA', v-model='securityCode', :placeholder='$t("auth:tfa.placeholder")', @keyup.enter='verifySecurityCode')
button.button.is-blue.is-fullwidth(@click='verifySecurityCode')
span {{ $t('auth:tfa.verifyToken') }}
.login-copyright
span {{ $t('footer.poweredby') }}
span {{ $t('footer.poweredBy') }}
a(href='https://wiki.js.org', rel='external', title='Wiki.js') Wiki.js
</template>
@@ -23,26 +32,36 @@
export default {
name: 'login',
data() {
data () {
return {
error: false,
strategies: [],
selectedStrategy: 'local'
selectedStrategy: 'local',
screen: 'login',
username: '',
password: '',
securityCode: '',
loginToken: '',
isLoading: false
}
},
computed: {
siteTitle() {
siteTitle () {
return siteConfig.title
}
},
methods: {
selectStrategy(key, useForm) {
selectStrategy (key, useForm) {
this.selectedStrategy = key
this.screen = 'login'
if (!useForm) {
window.location.assign(siteConfig.path + '/login/' + key)
window.location.assign(siteConfig.path + 'login/' + key)
} else {
this.$refs.iptEmail.focus()
}
},
refreshStrategies() {
refreshStrategies () {
this.isLoading = true
graphQL.query({
query: CONSTANTS.GRAPHQL.GQL_QUERY_AUTHENTICATION,
variables: {
@@ -54,19 +73,122 @@ export default {
} else {
throw new Error('No authentication providers available!')
}
this.isLoading = false
}).catch(err => {
console.error(err)
this.$store.dispatch('alert', {
style: 'error',
icon: 'gg-warning',
msg: err.message
})
this.isLoading = false
})
},
login() {
this.$store.dispatch('alert', {
style: 'error',
icon: 'gg-warning',
msg: 'Email or password is invalid'
})
login () {
if (this.username.length < 2) {
this.$store.dispatch('alert', {
style: 'error',
icon: 'gg-warning',
msg: 'Enter a valid email / username.'
})
this.$refs.iptEmail.focus()
} else if (this.password.length < 2) {
this.$store.dispatch('alert', {
style: 'error',
icon: 'gg-warning',
msg: 'Enter a valid password.'
})
this.$refs.iptPassword.focus()
} else {
this.isLoading = true
graphQL.mutate({
mutation: CONSTANTS.GRAPHQL.GQL_MUTATION_LOGIN,
variables: {
username: this.username,
password: this.password,
provider: this.selectedStrategy
}
}).then(resp => {
if (resp.data.login) {
let respObj = resp.data.login
if (respObj.succeeded === true) {
if (respObj.tfaRequired === true) {
this.screen = 'tfa'
this.securityCode = ''
this.loginToken = respObj.tfaLoginToken
this.$nextTick(() => {
this.$refs.iptTFA.focus()
})
} else {
this.$store.dispatch('alert', {
style: 'success',
icon: 'gg-check',
msg: 'Login successful!'
})
}
this.isLoading = false
} else {
throw new Error(respObj.message)
}
} else {
throw new Error('Authentication is unavailable.')
}
}).catch(err => {
console.error(err)
this.$store.dispatch('alert', {
style: 'error',
icon: 'gg-warning',
msg: err.message
})
this.isLoading = false
})
}
},
verifySecurityCode () {
if (this.securityCode.length !== 6) {
this.$store.dispatch('alert', {
style: 'error',
icon: 'gg-warning',
msg: 'Enter a valid security code.'
})
this.$refs.iptTFA.focus()
} else {
this.isLoading = true
graphQL.mutate({
mutation: CONSTANTS.GRAPHQL.GQL_MUTATION_LOGINTFA,
variables: {
loginToken: this.loginToken,
securityCode: this.securityCode
}
}).then(resp => {
if (resp.data.loginTFA) {
let respObj = resp.data.loginTFA
if (respObj.succeeded === true) {
this.$store.dispatch('alert', {
style: 'success',
icon: 'gg-check',
msg: 'Login successful!'
})
this.isLoading = false
} else {
throw new Error(respObj.message)
}
} else {
throw new Error('Authentication is unavailable.')
}
}).catch(err => {
console.error(err)
this.$store.dispatch('alert', {
style: 'error',
icon: 'gg-warning',
msg: err.message
})
this.isLoading = false
})
}
}
},
mounted() {
mounted () {
this.$store.commit('navigator/subtitleStatic', 'Login')
this.refreshStrategies()
this.$refs.iptEmail.focus()

View File

@@ -18,5 +18,23 @@ export default {
value
}
}
`,
GQL_MUTATION_LOGIN: gql`
mutation($username: String!, $password: String!, $provider: String!) {
login(username: $username, password: $password, provider: $provider) {
succeeded
message
tfaRequired
tfaLoginToken
}
}
`,
GQL_MUTATION_LOGINTFA: gql`
mutation($loginToken: String!, $securityCode: String!) {
loginTFA(loginToken: $loginToken, securityCode: $securityCode) {
succeeded
message
}
}
`
}

View File

@@ -54,6 +54,15 @@
border-radius: 6px;
animation: zoomIn .5s ease;
&::after {
position: absolute;
top: 1rem;
right: 1rem;
content: " ";
@include spinner(mc('blue', '500'),0.5s,16px);
display: none;
}
&.is-expanded {
width: 650px;
@@ -67,6 +76,10 @@
}
}
&.is-loading::after {
display: block;
}
@include until($tablet) {
width: 100%;
border-radius: 0;
@@ -264,6 +277,16 @@
}
&-tfa {
position: relative;
display: flex;
width: 400px;
align-items: stretch;
box-shadow: 0 14px 28px rgba(0,0,0,0.2);
border-radius: 6px;
animation: zoomIn .5s ease;
}
&-copyright {
display: flex;
align-items: center;