feat: auth jwt, permissions, login ui (wip)

This commit is contained in:
Nicolas Giard
2018-10-08 00:17:31 -04:00
parent 563d1a4f98
commit 3abd2f917c
53 changed files with 550 additions and 438 deletions

View File

@@ -1,14 +1,14 @@
<template lang="pug">
v-footer.justify-center(:color='color', inset)
v-footer.justify-center(:color='bgColor', inset)
.caption.grey--text.text--darken-1
span(v-if='company && company.length > 0') {{ $t('common:footer.copyright', { company: company, year: currentYear, interpolation: { escapeValue: false } }) }} |&nbsp;
span {{ $t('common:footer.poweredBy') }} #[a(href='https://wiki.js.org', ref='nofollow') Wiki.js]
v-snackbar(
:color='notification.style'
bottom,
right,
multi-line,
bottom
right
multi-line
v-model='notificationState'
)
.text-xs-left
@@ -21,9 +21,13 @@ import { get, sync } from 'vuex-pathify'
export default {
props: {
altbg: {
type: Boolean,
default: false
color: {
type: String,
default: 'grey lighten-3'
},
darkColor: {
type: String,
default: 'grey darken-3'
}
},
data() {
@@ -36,13 +40,11 @@ export default {
notification: get('notification'),
darkMode: get('site/dark'),
notificationState: sync('notification@isActive'),
color() {
if (this.altbg) {
return 'altbg'
} else if (!this.darkMode) {
return 'grey lighten-3'
bgColor() {
if (!this.darkMode) {
return this.color
} else {
return ''
return this.darkColor
}
}
}

View File

@@ -103,7 +103,7 @@
v-list-tile(href='/p')
v-list-tile-action: v-icon(color='red') person
v-list-tile-title Profile
v-list-tile(href='/logout')
v-list-tile(@click='logout')
v-list-tile-action: v-icon(color='red') exit_to_app
v-list-tile-title Logout
</template>
@@ -111,6 +111,7 @@
<script>
import { get } from 'vuex-pathify'
import _ from 'lodash'
import Cookies from 'js-cookie'
export default {
props: {
@@ -169,6 +170,10 @@ export default {
},
pageDelete () {
},
logout () {
Cookies.remove('jwt')
window.location.assign('/')
}
}
}

View File

@@ -1,47 +1,114 @@
<template lang="pug">
v-app
.login
.login-container(:class='{ "is-expanded": strategies.length > 1, "is-loading": isLoading }')
.login-mascot
img(src='/svg/henry-reading.svg', alt='Henry')
.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(v-show='screen === "login"')
h1.text-xs-center.display-1 {{ siteTitle }}
h2.text-xs-center.subheading {{ $t('auth:loginRequired') }}
v-text-field(solo, hide-details, ref='iptEmail', v-model='username', :placeholder='$t("auth:fields.emailUser")')
v-text-field.mt-2(
solo
hide-details
ref='iptPassword'
v-model='password'
:append-icon='hidePassword ? "visibility" : "visibility_off"'
@click:append='() => (hidePassword = !hidePassword)'
:type='hidePassword ? "password" : "text"'
:placeholder='$t("auth:fields.password")'
@keyup.enter='login'
)
v-btn.mt-3(block, large, color='primary', @click='login') {{ $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') }}
nav-footer(altbg)
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.radius-7(v-show='isShown')
v-toolbar(color='primary', flat, dense, dark)
v-spacer
.subheading(v-if='screen === "tfa"') {{ $t('auth:tfa.subtitle') }}
.subheading(v-else-if='selectedStrategy.key !== "local"') Login using {{ selectedStrategy.title }}
.subheading(v-else) {{ $t('auth:loginRequired') }}
v-spacer
v-card-text.text-xs-center
h1.display-1.primary--text.py-2 {{ siteTitle }}
template(v-if='screen === "login"')
v-text-field.md2.mt-3(
solo
flat
prepend-icon='email'
background-color='grey lighten-4'
hide-details
ref='iptEmail'
v-model='username'
:placeholder='$t("auth:fields.emailUser")'
)
v-text-field.md2.mt-2(
solo
flat
prepend-icon='vpn_key'
background-color='grey lighten-4'
hide-details
ref='iptPassword'
v-model='password'
:append-icon='hidePassword ? "visibility" : "visibility_off"'
@click:append='() => (hidePassword = !hidePassword)'
:type='hidePassword ? "password" : "text"'
:placeholder='$t("auth:fields.password")'
@keyup.enter='login'
)
template(v-if='screen === "tfa"')
.body-2 Enter the security code generated from your trusted device:
v-text-field.md2.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'
)
v-card-actions.pb-4
v-spacer
v-btn(
v-if='screen === "login"'
block
large
color='primary'
@click='login'
round
:loading='isLoading'
) {{ $t('auth:actions.login') }}
v-btn(
v-if='screen === "tfa"'
block
large
color='primary'
@click='verifySecurityCode'
round
:loading='isLoading'
) {{ $t('auth:tfa.verifyToken') }}
v-spacer
v-card-actions.pb-3(v-if='selectedStrategy.key === "local"')
v-spacer
a.caption(href='') Forgot your password?
v-spacer
template(v-if='isSocialShown')
v-divider
v-card-text.grey.lighten-4.text-xs-center
.pb-2.body-2.text-xs-center.grey--text.text--darken-2 or login using...
v-tooltip(top, v-for='strategy in strategies', :key='strategy.key')
.social-login-btn.mr-2(
slot='activator'
v-ripple
v-html='strategy.icon'
:class='strategy.color + " elevation-" + (strategy.key === selectedStrategy.key ? "0" : "4")'
@click='selectStrategy(strategy)'
)
span {{ strategy.title }}
template(v-if='selectedStrategy.selfRegistration')
v-divider
v-card-actions.py-3(:class='isSocialShown ? "" : "grey lighten-4"')
v-spacer
.caption Don't have an account yet? #[a.caption(href='') Create an account]
v-spacer
nav-footer(color='grey darken-4')
</template>
<script>
/* global siteConfig */
import _ from 'lodash'
import { mapState } from 'vuex'
import Cookies from 'js-cookie'
import strategiesQuery from 'gql/login/login-query-strategies.gql'
import loginMutation from 'gql/login/login-mutation-login.gql'
@@ -53,41 +120,50 @@ export default {
return {
error: false,
strategies: [],
selectedStrategy: 'local',
selectedStrategy: { key: 'local' },
screen: 'login',
username: '',
password: '',
hidePassword: true,
securityCode: '',
loginToken: '',
isLoading: false
isLoading: false,
isShown: false
}
},
computed: {
...mapState(['notification']),
notificationState: {
get() { return this.notification.isActive },
set(newState) { this.$store.commit('updateNotificationState', newState) }
},
siteTitle () {
return siteConfig.title
},
isSocialShown () {
return this.strategies.length > 1
}
},
watch: {
strategies(newValue, oldValue) {
this.selectedStrategy = _.find(newValue, ['key', 'local'])
}
},
mounted () {
this.$refs.iptEmail.focus()
this.isShown = true
this.$nextTick(() => {
this.$refs.iptEmail.focus()
})
},
methods: {
/**
* SELECT STRATEGY
*/
selectStrategy (key, useForm) {
this.selectedStrategy = key
selectStrategy (strategy) {
this.selectedStrategy = strategy
this.screen = 'login'
if (!useForm) {
if (!strategy.useForm) {
this.isLoading = true
window.location.assign(this.$helpers.resolvePath('login/' + key))
window.location.assign(this.$helpers.resolvePath('login/' + strategy.key))
} else {
this.$refs.iptEmail.focus()
this.$nextTick(() => {
this.$refs.iptEmail.focus()
})
}
},
/**
@@ -116,7 +192,7 @@ export default {
variables: {
username: this.username,
password: this.password,
strategy: this.selectedStrategy
strategy: this.selectedStrategy.key
}
})
if (_.has(resp, 'data.authentication.login')) {
@@ -135,6 +211,7 @@ export default {
style: 'success',
icon: 'check'
})
Cookies.set('jwt', respObj.jwt, { expires: 365 })
_.delay(() => {
window.location.replace('/') // TEMPORARY - USE RETURNURL
}, 1000)
@@ -222,15 +299,12 @@ export default {
<style lang="scss">
.login {
background-color: mc('blue', '800');
background-color: mc('grey', '900');
background-image: url('../static/svg/motif-blocks.svg');
background-repeat: repeat;
background-size: 200px;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
animation: loginBgReveal 20s linear infinite;
@include keyframes(loginBgReveal) {
@@ -245,7 +319,6 @@ export default {
&::before {
content: '';
position: absolute;
background-color: #0d47a1;
background-image: url('../static/svg/motif-overlay.svg');
background-attachment: fixed;
background-size: cover;
@@ -256,259 +329,41 @@ export default {
height: 100vh;
}
&::after {
content: '';
position: absolute;
background-image: linear-gradient(to bottom, rgba(mc('blue', '800'), .9) 0%, rgba(mc('blue', '800'), 0) 100%);
top: 0;
left: 0;
width: 100vw;
height: 25vh;
z-index: 1;
}
&-mascot {
width: 200px;
height: 200px;
position: absolute;
top: -180px;
left: 50%;
margin-left: -100px;
z-index: 10;
@include until($tablet) {
display: none;
}
}
&-container {
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;
z-index: 2;
&::after {
position: absolute;
top: 1rem;
right: 1rem;
content: " ";
@include spinner(mc('blue', '500'),0.5s,16px);
display: none;
}
&.is-expanded {
width: 650px;
.login-frame {
border-radius: 0 6px 6px 0;
border-left: none;
@include until($tablet) {
border-radius: 0;
}
}
}
&.is-loading::after {
display: block;
}
@include until($tablet) {
width: 95vw;
border-radius: 0;
&.is-expanded {
width: 95vw;
}
}
}
&-providers {
display: flex;
flex-direction: column;
width: 250px;
border-right: none;
border-radius: 6px 0 0 6px;
z-index: 1;
overflow: hidden;
@include until($tablet) {
width: 50px;
border-radius: 0;
}
button {
flex: 0 1 50px;
padding: 5px 15px;
border: none;
color: #FFF;
// background: linear-gradient(to right, rgba(mc('light-blue', '800'), .7), rgba(mc('light-blue', '800'), 1));
// border-top: 1px solid rgba(mc('light-blue', '900'), .5);
background: linear-gradient(to right, rgba(0,0,0, .5), rgba(0,0,0, .7));
border-top: 1px solid rgba(0,0,0, .2);
font-weight: 600;
text-align: left;
min-height: 40px;
display: flex;
justify-content: flex-start;
align-items: center;
transition: all .4s ease;
&:focus {
outline: none;
}
@include until($tablet) {
justify-content: center;
}
&:hover {
background-color: rgba(0,0,0, .4);
}
&:first-child {
border-top: none;
&.is-active {
border-top: 1px solid rgba(255,255,255, .5);
}
}
&.is-active {
background-image: linear-gradient(to right, rgba(255,255,255,1) 0%,rgba(255,255,255,.77) 100%);
color: mc('grey', '800');
cursor: default;
&:hover {
background-color: transparent;
}
svg path {
fill: mc('grey', '800');
}
}
i {
margin-right: 10px;
font-size: 16px;
@include until($tablet) {
margin-right: 0;
font-size: 20px;
}
}
svg {
margin-right: 10px;
width: auto;
height: 20px;
max-width: 18px;
max-height: 20px;
path {
fill: #FFF;
}
@include until($tablet) {
margin-right: 0;
font-size: 20px;
}
}
em {
height: 20px;
}
span {
font-weight: 600;
@include until($tablet) {
display: none;
}
}
}
&-fill {
flex: 1 1 0;
background: linear-gradient(to right, rgba(mc('light-blue', '800'), .7), rgba(mc('light-blue', '800'), 1));
}
}
&-frame {
background-image: radial-gradient(circle at top center, rgba(255,255,255,1) 5%,rgba(255,255,255,.6) 100%);
border: 1px solid rgba(255,255,255, .5);
border-radius: 6px;
width: 400px;
padding: 1rem;
color: mc('grey', '700');
display: block;
@include until($tablet) {
width: 100%;
border-radius: 0;
border: none;
}
h1 {
font-size: 2rem;
font-weight: 400;
color: mc('light-blue', '700');
text-shadow: 1px 1px 0 #FFF;
padding: 1rem 0 0 0;
margin: 0;
}
h2 {
font-size: 1.5rem;
font-weight: 300;
color: mc('grey', '700');
text-shadow: 1px 1px 0 #FFF;
padding: 0;
margin: 0 0 25px 0;
}
}
&-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;
> .container {
height: 100%;
align-items: center;
display: flex;
}
h1 {
font-family: 'Varela Round' !important;
}
.social-login-btn {
display: inline-flex;
justify-content: center;
position: absolute;
left: 0;
bottom: 10vh;
width: 100%;
z-index: 2;
color: mc('grey', '500');
font-weight: 400;
a {
font-weight: 600;
color: mc('blue', '500');
margin-left: .25rem;
@include until($tablet) {
color: mc('blue', '200');
align-items: center;
border-radius: 50%;
width: 54px;
height: 54px;
cursor: pointer;
transition: opacity .2s ease;
&:hover {
opacity: .8;
}
margin: .5rem 0;
svg {
width: 24px;
height: 24px;
bottom: 0;
path {
fill: #FFF;
}
}
}
@include until($tablet) {
color: mc('blue', '50');
}
.v-text-field.centered input {
text-align: center;
}
}
</style>