|
|
|
<template lang="pug">
|
|
|
|
v-app
|
|
|
|
.login
|
|
|
|
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='fadeUp')
|
|
|
|
v-card.elevation-5(v-show='isShown', light)
|
|
|
|
v-toolbar(color='indigo', flat, dense, dark)
|
|
|
|
v-spacer
|
|
|
|
.subheading(v-if='screen === "tfa"') {{ $t('auth:tfa.subtitle') }}
|
|
|
|
.subheading(v-if='screen === "changePwd"') {{ $t('auth:changePwd.subtitle') }}
|
|
|
|
.subheading(v-else-if='selectedStrategy.key !== "local"') {{ $t('auth:loginUsingStrategy', { strategy: selectedStrategy.title, interpolation: { escapeValue: false } }) }}
|
|
|
|
.subheading(v-else) {{ $t('auth:loginRequired') }}
|
|
|
|
v-spacer
|
|
|
|
v-card-text.text-center
|
|
|
|
h1.display-1.indigo--text.py-2 {{ siteTitle }}
|
|
|
|
template(v-if='screen === "login"')
|
|
|
|
v-text-field.mt-3(
|
|
|
|
solo
|
|
|
|
flat
|
|
|
|
prepend-icon='mdi-clipboard-account'
|
|
|
|
background-color='grey lighten-4'
|
|
|
|
hide-details
|
|
|
|
ref='iptEmail'
|
|
|
|
v-model='username'
|
|
|
|
:placeholder='$t("auth:fields.emailUser")'
|
|
|
|
)
|
|
|
|
v-text-field.mt-2(
|
|
|
|
solo
|
|
|
|
flat
|
|
|
|
prepend-icon='mdi-textbox-password'
|
|
|
|
background-color='grey lighten-4'
|
|
|
|
hide-details
|
|
|
|
ref='iptPassword'
|
|
|
|
v-model='password'
|
|
|
|
:append-icon='hidePassword ? "mdi-eye-off" : "mdi-eye"'
|
|
|
|
@click:append='() => (hidePassword = !hidePassword)'
|
|
|
|
:type='hidePassword ? "password" : "text"'
|
|
|
|
:placeholder='$t("auth:fields.password")'
|
|
|
|
@keyup.enter='login'
|
|
|
|
)
|
|
|
|
template(v-else-if='screen === "tfa"')
|
|
|
|
.body-2 Enter the security code generated from your trusted device:
|
|
|
|
v-text-field.centered.mt-2(
|
|
|
|
solo
|
|
|
|
flat
|
|
|
|
background-color='grey lighten-4'
|
|
|
|
hide-details
|
|
|
|
ref='iptTFA'
|
|
|
|
v-model='securityCode'
|
|
|
|
:placeholder='$t("auth:tfa.placeholder")'
|
|
|
|
@keyup.enter='verifySecurityCode'
|
|
|
|
)
|
|
|
|
template(v-else-if='screen === "changePwd"')
|
|
|
|
.body-2 {{$t('auth:changePwd.instructions')}}
|
|
|
|
v-text-field.mt-2(
|
|
|
|
type='password'
|
|
|
|
solo
|
|
|
|
flat
|
|
|
|
background-color='grey lighten-4'
|
|
|
|
hide-details
|
|
|
|
ref='iptNewPassword'
|
|
|
|
v-model='newPassword'
|
|
|
|
:placeholder='$t(`auth:changePwd.newPasswordPlaceholder`)'
|
|
|
|
)
|
|
|
|
v-text-field.mt-2(
|
|
|
|
type='password'
|
|
|
|
solo
|
|
|
|
flat
|
|
|
|
background-color='grey lighten-4'
|
|
|
|
hide-details
|
|
|
|
v-model='newPasswordVerify'
|
|
|
|
:placeholder='$t(`auth:changePwd.newPasswordVerifyPlaceholder`)'
|
|
|
|
@keyup.enter='changePassword'
|
|
|
|
)
|
|
|
|
template(v-else-if='screen === "forgot"')
|
|
|
|
.body-2 {{ $t('auth:forgotPasswordSubtitle') }}
|
|
|
|
v-text-field.mt-3(
|
|
|
|
solo
|
|
|
|
flat
|
|
|
|
prepend-icon='mdi-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(
|
|
|
|
width='100%'
|
|
|
|
max-width='250px'
|
|
|
|
v-if='screen === "login"'
|
|
|
|
large
|
|
|
|
color='primary'
|
|
|
|
dark
|
|
|
|
@click='login'
|
|
|
|
rounded
|
|
|
|
:loading='isLoading'
|
|
|
|
) {{ $t('auth:actions.login') }}
|
|
|
|
v-btn(
|
|
|
|
width='100%'
|
|
|
|
max-width='250px'
|
|
|
|
v-else-if='screen === "tfa"'
|
|
|
|
large
|
|
|
|
color='primary'
|
|
|
|
dark
|
|
|
|
@click='verifySecurityCode'
|
|
|
|
rounded
|
|
|
|
:loading='isLoading'
|
|
|
|
) {{ $t('auth:tfa.verifyToken') }}
|
|
|
|
v-btn(
|
|
|
|
width='100%'
|
|
|
|
max-width='250px'
|
|
|
|
v-else-if='screen === "changePwd"'
|
|
|
|
large
|
|
|
|
color='primary'
|
|
|
|
dark
|
|
|
|
@click='changePassword'
|
|
|
|
rounded
|
|
|
|
:loading='isLoading'
|
|
|
|
) {{ $t('auth:changePwd.proceed') }}
|
|
|
|
v-btn(
|
|
|
|
width='100%'
|
|
|
|
max-width='250px'
|
|
|
|
v-else-if='screen === "forgot"'
|
|
|
|
large
|
|
|
|
color='primary'
|
|
|
|
dark
|
|
|
|
@click='forgotPasswordSubmit'
|
|
|
|
rounded
|
|
|
|
:loading='isLoading'
|
|
|
|
) {{ $t('auth:sendResetPassword') }}
|
|
|
|
v-spacer
|
|
|
|
v-card-actions.pb-3(v-if='screen === "login" && selectedStrategy.key === "local"')
|
|
|
|
v-spacer
|
|
|
|
a.caption(@click.stop.prevent='forgotPassword', href='#forgot') {{ $t('auth:forgotPasswordLink') }}
|
|
|
|
v-spacer
|
|
|
|
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-center
|
|
|
|
.pb-2.body-2.text-xs-center.grey--text.text--darken-2 {{ $t('auth:orLoginUsingStrategy') }}
|
|
|
|
v-btn.mx-1.social-login-btn(
|
|
|
|
v-for='strategy in strategies', :key='strategy.key'
|
|
|
|
large
|
|
|
|
@click='selectStrategy(strategy)'
|
|
|
|
dark
|
|
|
|
:color='strategy.color'
|
|
|
|
:depressed='strategy.key === selectedStrategy.key'
|
|
|
|
)
|
|
|
|
v-avatar.mr-3(tile, :class='strategy.color', size='24', v-html='strategy.icon')
|
|
|
|
span(style='text-transform: none;') {{ strategy.title }}
|
|
|
|
template(v-if='screen === "login" && selectedStrategy.key === `local` && selectedStrategy.selfRegistration')
|
|
|
|
v-divider
|
|
|
|
v-card-actions.py-3(:class='isSocialShown ? "" : "grey lighten-4"')
|
|
|
|
v-spacer
|
|
|
|
i18next.caption(path='auth:switchToRegister.text', tag='div')
|
|
|
|
a.caption(href='/register', place='link') {{ $t('auth:switchToRegister.link') }}
|
|
|
|
v-spacer
|
|
|
|
|
|
|
|
loader(v-model='isLoading', :color='loaderColor', :title='loaderTitle', :subtitle='$t(`auth:pleaseWait`)')
|
|
|
|
nav-footer(color='grey darken-5', dark-color='grey darken-5')
|
|
|
|
notify
|
|
|
|
</template>
|
|
|
|
|
|
|
|
<script>
|
|
|
|
/* global siteConfig */
|
|
|
|
|
|
|
|
import _ from 'lodash'
|
|
|
|
import Cookies from 'js-cookie'
|
|
|
|
|
|
|
|
import strategiesQuery from 'gql/login/login-query-strategies.gql'
|
|
|
|
import loginMutation from 'gql/login/login-mutation-login.gql'
|
|
|
|
import tfaMutation from 'gql/login/login-mutation-tfa.gql'
|
|
|
|
import changePasswordMutation from 'gql/login/login-mutation-changepassword.gql'
|
|
|
|
|
|
|
|
export default {
|
|
|
|
i18nOptions: { namespaces: 'auth' },
|
|
|
|
data () {
|
|
|
|
return {
|
|
|
|
error: false,
|
|
|
|
strategies: [],
|
|
|
|
selectedStrategy: { key: 'local' },
|
|
|
|
screen: 'login',
|
|
|
|
username: '',
|
|
|
|
password: '',
|
|
|
|
hidePassword: true,
|
|
|
|
securityCode: '',
|
|
|
|
continuationToken: '',
|
|
|
|
isLoading: false,
|
|
|
|
loaderColor: 'grey darken-4',
|
|
|
|
loaderTitle: 'Working...',
|
|
|
|
isShown: false,
|
|
|
|
newPassword: '',
|
|
|
|
newPasswordVerify: ''
|
|
|
|
}
|
|
|
|
},
|
|
|
|
computed: {
|
|
|
|
siteTitle () {
|
|
|
|
return siteConfig.title
|
|
|
|
},
|
|
|
|
isSocialShown () {
|
|
|
|
return this.strategies.length > 1
|
|
|
|
}
|
|
|
|
},
|
|
|
|
watch: {
|
|
|
|
strategies(newValue, oldValue) {
|
|
|
|
this.selectedStrategy = _.find(newValue, ['key', 'local'])
|
|
|
|
}
|
|
|
|
},
|
|
|
|
mounted () {
|
|
|
|
this.isShown = true
|
|
|
|
this.$nextTick(() => {
|
|
|
|
this.$refs.iptEmail.focus()
|
|
|
|
})
|
|
|
|
},
|
|
|
|
methods: {
|
|
|
|
/**
|
|
|
|
* SELECT STRATEGY
|
|
|
|
*/
|
|
|
|
selectStrategy (strategy) {
|
|
|
|
this.selectedStrategy = strategy
|
|
|
|
this.screen = 'login'
|
|
|
|
if (!strategy.useForm) {
|
|
|
|
this.isLoading = true
|
|
|
|
window.location.assign('/login/' + strategy.key)
|
|
|
|
} else {
|
|
|
|
this.$nextTick(() => {
|
|
|
|
this.$refs.iptEmail.focus()
|
|
|
|
})
|
|
|
|
}
|
|
|
|
},
|
|
|
|
/**
|
|
|
|
* LOGIN
|
|
|
|
*/
|
|
|
|
async login () {
|
|
|
|
if (this.username.length < 2) {
|
|
|
|
this.$store.commit('showNotification', {
|
|
|
|
style: 'red',
|
|
|
|
message: this.$t('auth:invalidEmailUsername'),
|
|
|
|
icon: 'alert'
|
|
|
|
})
|
|
|
|
this.$refs.iptEmail.focus()
|
|
|
|
} else if (this.password.length < 2) {
|
|
|
|
this.$store.commit('showNotification', {
|
|
|
|
style: 'red',
|
|
|
|
message: this.$t('auth:invalidPassword'),
|
|
|
|
icon: 'alert'
|
|
|
|
})
|
|
|
|
this.$refs.iptPassword.focus()
|
|
|
|
} else {
|
|
|
|
this.loaderColor = 'grey darken-4'
|
|
|
|
this.loaderTitle = this.$t('auth:signingIn')
|
|
|
|
this.isLoading = true
|
|
|
|
try {
|
|
|
|
let resp = await this.$apollo.mutate({
|
|
|
|
mutation: loginMutation,
|
|
|
|
variables: {
|
|
|
|
username: this.username,
|
|
|
|
password: this.password,
|
|
|
|
strategy: this.selectedStrategy.key
|
|
|
|
}
|
|
|
|
})
|
|
|
|
if (_.has(resp, 'data.authentication.login')) {
|
|
|
|
let respObj = _.get(resp, 'data.authentication.login', {})
|
|
|
|
if (respObj.responseResult.succeeded === true) {
|
|
|
|
this.continuationToken = respObj.continuationToken
|
|
|
|
if (respObj.mustChangePwd === true) {
|
|
|
|
this.screen = 'changePwd'
|
|
|
|
this.$nextTick(() => {
|
|
|
|
this.$refs.iptNewPassword.focus()
|
|
|
|
})
|
|
|
|
this.isLoading = false
|
|
|
|
} else if (respObj.mustProvideTFA === true) {
|
|
|
|
this.screen = 'tfa'
|
|
|
|
this.securityCode = ''
|
|
|
|
this.$nextTick(() => {
|
|
|
|
this.$refs.iptTFA.focus()
|
|
|
|
})
|
|
|
|
this.isLoading = false
|
|
|
|
} else {
|
|
|
|
this.loaderColor = 'green darken-1'
|
|
|
|
this.loaderTitle = this.$t('auth:loginSuccess')
|
|
|
|
Cookies.set('jwt', respObj.jwt, { expires: 365 })
|
|
|
|
_.delay(() => {
|
|
|
|
const loginRedirect = Cookies.get('loginRedirect')
|
|
|
|
if (loginRedirect) {
|
|
|
|
Cookies.remove('loginRedirect')
|
|
|
|
window.location.replace(loginRedirect)
|
|
|
|
} else {
|
|
|
|
window.location.replace('/')
|
|
|
|
}
|
|
|
|
}, 1000)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
throw new Error(respObj.responseResult.message)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
throw new Error(this.$t('auth:genericError'))
|
|
|
|
}
|
|
|
|
} catch (err) {
|
|
|
|
console.error(err)
|
|
|
|
this.$store.commit('showNotification', {
|
|
|
|
style: 'red',
|
|
|
|
message: err.message,
|
|
|
|
icon: 'alert'
|
|
|
|
})
|
|
|
|
this.isLoading = false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
/**
|
|
|
|
* VERIFY TFA CODE
|
|
|
|
*/
|
|
|
|
verifySecurityCode () {
|
|
|
|
if (this.securityCode.length !== 6) {
|
|
|
|
this.$store.commit('showNotification', {
|
|
|
|
style: 'red',
|
|
|
|
message: 'Enter a valid security code.',
|
|
|
|
icon: 'warning'
|
|
|
|
})
|
|
|
|
this.$refs.iptTFA.focus()
|
|
|
|
} else {
|
|
|
|
this.isLoading = true
|
|
|
|
this.$apollo.mutate({
|
|
|
|
mutation: tfaMutation,
|
|
|
|
variables: {
|
|
|
|
continuationToken: this.continuationToken,
|
|
|
|
securityCode: this.securityCode
|
|
|
|
}
|
|
|
|
}).then(resp => {
|
|
|
|
if (_.has(resp, 'data.authentication.loginTFA')) {
|
|
|
|
let respObj = _.get(resp, 'data.authentication.loginTFA', {})
|
|
|
|
if (respObj.responseResult.succeeded === true) {
|
|
|
|
this.$store.commit('showNotification', {
|
|
|
|
message: 'Login successful!',
|
|
|
|
style: 'success',
|
|
|
|
icon: 'check'
|
|
|
|
})
|
|
|
|
_.delay(() => {
|
|
|
|
window.location.replace('/') // TEMPORARY - USE RETURNURL
|
|
|
|
}, 1000)
|
|
|
|
this.isLoading = false
|
|
|
|
} else {
|
|
|
|
throw new Error(respObj.responseResult.message)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
throw new Error(this.$t('auth:genericError'))
|
|
|
|
}
|
|
|
|
}).catch(err => {
|
|
|
|
console.error(err)
|
|
|
|
this.$store.commit('showNotification', {
|
|
|
|
style: 'red',
|
|
|
|
message: err.message,
|
|
|
|
icon: 'alert'
|
|
|
|
})
|
|
|
|
this.isLoading = false
|
|
|
|
})
|
|
|
|
}
|
|
|
|
},
|
|
|
|
/**
|
|
|
|
* CHANGE PASSWORD
|
|
|
|
*/
|
|
|
|
async changePassword () {
|
|
|
|
this.loaderColor = 'grey darken-4'
|
|
|
|
this.loaderTitle = this.$t('auth:changePwd.loading')
|
|
|
|
this.isLoading = true
|
|
|
|
const resp = await this.$apollo.mutate({
|
|
|
|
mutation: changePasswordMutation,
|
|
|
|
variables: {
|
|
|
|
continuationToken: this.continuationToken,
|
|
|
|
newPassword: this.newPassword
|
|
|
|
}
|
|
|
|
})
|
|
|
|
if (_.get(resp, 'data.authentication.loginChangePassword.responseResult.succeeded', false) === true) {
|
|
|
|
this.loaderColor = 'green darken-1'
|
|
|
|
this.loaderTitle = this.$t('auth:loginSuccess')
|
|
|
|
Cookies.set('jwt', _.get(resp, 'data.authentication.loginChangePassword.jwt', ''), { expires: 365 })
|
|
|
|
_.delay(() => {
|
|
|
|
window.location.replace('/') // TEMPORARY - USE RETURNURL
|
|
|
|
}, 1000)
|
|
|
|
} else {
|
|
|
|
this.$store.commit('showNotification', {
|
|
|
|
style: 'red',
|
|
|
|
message: _.get(resp, 'data.authentication.loginChangePassword.responseResult.message', false),
|
|
|
|
icon: 'alert'
|
|
|
|
})
|
|
|
|
this.isLoading = false
|
|
|
|
}
|
|
|
|
},
|
|
|
|
/**
|
|
|
|
* SWITCH TO FORGOT PASSWORD SCREEN
|
|
|
|
*/
|
|
|
|
forgotPassword () {
|
|
|
|
this.screen = 'forgot'
|
|
|
|
this.$nextTick(() => {
|
|
|
|
this.$refs.iptEmailForgot.focus()
|
|
|
|
})
|
|
|
|
},
|
|
|
|
/**
|
|
|
|
* FORGOT PASSWORD SUBMIT
|
|
|
|
*/
|
|
|
|
async forgotPasswordSubmit () {
|
|
|
|
this.$store.commit('showNotification', {
|
|
|
|
style: 'pink',
|
|
|
|
message: 'Coming soon!',
|
|
|
|
icon: 'ferry'
|
|
|
|
})
|
|
|
|
}
|
|
|
|
},
|
|
|
|
apollo: {
|
|
|
|
strategies: {
|
|
|
|
query: strategiesQuery,
|
|
|
|
update: (data) => data.authentication.strategies,
|
|
|
|
watchLoading (isLoading) {
|
|
|
|
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'login-strategies-refresh')
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
</script>
|
|
|
|
|
|
|
|
<style lang="scss">
|
|
|
|
.login {
|
|
|
|
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-y: 0;
|
|
|
|
}
|
|
|
|
100% {
|
|
|
|
background-position-y: 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;
|
|
|
|
}
|
|
|
|
|
|
|
|
.social-login-btn {
|
|
|
|
cursor: pointer;
|
|
|
|
transition: opacity .2s ease;
|
|
|
|
&:hover {
|
|
|
|
opacity: .8;
|
|
|
|
}
|
|
|
|
margin: .25rem 0;
|
|
|
|
svg {
|
|
|
|
width: 24px;
|
|
|
|
height: 24px;
|
|
|
|
bottom: 0;
|
|
|
|
path {
|
|
|
|
fill: #FFF;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.v-text-field.centered input {
|
|
|
|
text-align: center;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
</style>
|