mirror of https://github.com/requarks/wiki
parent
82ebac2dd6
commit
097833d77a
@ -1,12 +1,77 @@
|
|||||||
extends base.pug
|
doctype html
|
||||||
|
html
|
||||||
block body
|
head
|
||||||
#root.is-fullscreen
|
meta(charset="UTF-8")
|
||||||
.app-error
|
link(rel="icon" href="/favicon.ico")
|
||||||
a(href='/')
|
meta(name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width")
|
||||||
img(src='/_assets/svg/logo-wikijs.svg')
|
title Wiki.js
|
||||||
strong Oops, something went wrong...
|
link(href="/_assets/fonts/roboto/roboto.css" rel="stylesheet")
|
||||||
span= message
|
style(lang='text/scss').
|
||||||
|
body {
|
||||||
if error.stack
|
margin: 0;
|
||||||
pre: code #{error.stack}
|
font-family: "Roboto", "-apple-system", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.errorpage {
|
||||||
|
background:#070a0d radial-gradient(ellipse,#161b22,#070a0d);
|
||||||
|
color:#fff;
|
||||||
|
height:100vh;
|
||||||
|
}
|
||||||
|
.errorpage-bg {
|
||||||
|
position:absolute;
|
||||||
|
top:50%;
|
||||||
|
left:50%;
|
||||||
|
width:320px;
|
||||||
|
height:320px;
|
||||||
|
background:linear-gradient(0,transparent 50%,#c62828 50%);
|
||||||
|
border-radius:50%;
|
||||||
|
filter:blur(80px);
|
||||||
|
transform:translate(-50%,-50%);
|
||||||
|
visibility:hidden;
|
||||||
|
}
|
||||||
|
.errorpage-content {
|
||||||
|
position:absolute;
|
||||||
|
top:50%;
|
||||||
|
left:50%;
|
||||||
|
transform:translate(-50%,-50%);
|
||||||
|
display:flex;
|
||||||
|
flex-direction:column;
|
||||||
|
justify-content:center;
|
||||||
|
align-items:center;
|
||||||
|
}
|
||||||
|
.errorpage-code {
|
||||||
|
font-size:12rem;
|
||||||
|
line-height:12rem;
|
||||||
|
font-weight:700;
|
||||||
|
background:linear-gradient(45deg,#c62828,#ef9a9a);
|
||||||
|
-webkit-background-clip:text;
|
||||||
|
background-clip:text;
|
||||||
|
-webkit-text-fill-color:transparent;
|
||||||
|
-webkit-user-select:none;
|
||||||
|
user-select:none;
|
||||||
|
}
|
||||||
|
.errorpage-title {
|
||||||
|
font-size:80px;
|
||||||
|
font-weight:500;
|
||||||
|
line-height:80px;
|
||||||
|
}
|
||||||
|
.errorpage-hint {
|
||||||
|
font-size:1.2rem;
|
||||||
|
font-weight:500;
|
||||||
|
color:#ef9a9a;
|
||||||
|
line-height:1.2rem;
|
||||||
|
margin-top:1rem;
|
||||||
|
}
|
||||||
|
.errorpage-pre {
|
||||||
|
margin-top: 28px;
|
||||||
|
color: rgba(255,255,255,.5);
|
||||||
|
}
|
||||||
|
body
|
||||||
|
.errorpage
|
||||||
|
.errorpage-bg
|
||||||
|
.errorpage-content
|
||||||
|
.errorpage-code 500
|
||||||
|
.errorpage-title Server Error
|
||||||
|
.errorpage-hint= message
|
||||||
|
if error.stack
|
||||||
|
pre.errorpage-pre: code #{error.stack}
|
||||||
|
After Width: | Height: | Size: 51 KiB |
@ -0,0 +1,788 @@
|
|||||||
|
<template lang="pug">
|
||||||
|
.auth-login
|
||||||
|
//- -----------------------------------------------------
|
||||||
|
//- LOGIN SCREEN
|
||||||
|
//- -----------------------------------------------------
|
||||||
|
template(v-if='state.screen === `login`')
|
||||||
|
template(v-if='state.strategies?.length > 1')
|
||||||
|
p {{t('auth.selectAuthProvider')}}
|
||||||
|
.auth-strategies.q-mb-md
|
||||||
|
q-btn(
|
||||||
|
v-for='str of state.strategies'
|
||||||
|
:label='str.activeStrategy.displayName'
|
||||||
|
:icon='`img:` + str.activeStrategy.strategy.icon'
|
||||||
|
push
|
||||||
|
no-caps
|
||||||
|
:color='str.id === state.selectedStrategyId ? `primary` : ($q.dark.isActive ? `blue-grey-9` : `grey-1`)'
|
||||||
|
:text-color='str.id === state.selectedStrategyId || $q.dark.isActive ? `white` : `blue-grey-9`'
|
||||||
|
@click='state.selectedStrategyId = str.id'
|
||||||
|
)
|
||||||
|
q-form(ref='loginForm', @submit='login')
|
||||||
|
q-input(
|
||||||
|
ref='loginEmailIpt'
|
||||||
|
v-model='state.username'
|
||||||
|
autofocus
|
||||||
|
outlined
|
||||||
|
:label='t(`auth.fields.` + (selectedStrategy.activeStrategy?.strategy?.usernameType ?? `email`))'
|
||||||
|
:rules='selectedStrategy.activeStrategy?.strategy?.usernameType === `username` ? loginUsernameValidation : userEmailValidation'
|
||||||
|
lazy-rules='ondemand'
|
||||||
|
hide-bottom-space
|
||||||
|
:autocomplete='selectedStrategy.activeStrategy?.strategy?.usernameType ?? `email`'
|
||||||
|
)
|
||||||
|
template(#prepend)
|
||||||
|
i.las.la-user
|
||||||
|
q-input.q-mt-sm(
|
||||||
|
v-model='state.password'
|
||||||
|
outlined
|
||||||
|
:label='t(`auth.fields.password`)'
|
||||||
|
:rules='loginPasswordValidation'
|
||||||
|
lazy-rules='ondemand'
|
||||||
|
hide-bottom-space
|
||||||
|
type='password'
|
||||||
|
autocomplete='current-password'
|
||||||
|
)
|
||||||
|
template(#prepend)
|
||||||
|
i.las.la-key
|
||||||
|
q-btn.full-width.q-mt-sm(
|
||||||
|
type='submit'
|
||||||
|
push
|
||||||
|
color='primary'
|
||||||
|
:label='t(`auth.actions.login`)'
|
||||||
|
no-caps
|
||||||
|
icon='las la-sign-in-alt'
|
||||||
|
)
|
||||||
|
template(v-if='selectedStrategy.activeStrategy?.strategy?.key === `local`')
|
||||||
|
q-separator.q-my-md
|
||||||
|
q-btn.acrylic-btn.full-width(
|
||||||
|
flat
|
||||||
|
color='primary'
|
||||||
|
:label='t(`auth.switchToRegister.link`)'
|
||||||
|
no-caps
|
||||||
|
icon='las la-user-plus'
|
||||||
|
@click='switchTo(`register`)'
|
||||||
|
)
|
||||||
|
q-btn.acrylic-btn.full-width.q-mt-sm(
|
||||||
|
flat
|
||||||
|
color='primary'
|
||||||
|
:label='t(`auth.forgotPasswordLink`)'
|
||||||
|
no-caps
|
||||||
|
icon='las la-life-ring'
|
||||||
|
@click='switchTo(`forgot`)'
|
||||||
|
)
|
||||||
|
|
||||||
|
//- -----------------------------------------------------
|
||||||
|
//- FORGOT PASSWORD SCREEN
|
||||||
|
//- -----------------------------------------------------
|
||||||
|
template(v-else-if='state.screen === `forgot`')
|
||||||
|
p {{t('auth.forgotPasswordSubtitle')}}
|
||||||
|
q-form(ref='forgotForm', @submit='forgotPassword')
|
||||||
|
q-input(
|
||||||
|
ref='forgotEmailIpt'
|
||||||
|
v-model='state.username'
|
||||||
|
outlined
|
||||||
|
:rules='userEmailValidation'
|
||||||
|
lazy-rules='ondemand'
|
||||||
|
hide-bottom-space
|
||||||
|
:label='t(`auth.fields.email`)'
|
||||||
|
autocomplete='email'
|
||||||
|
)
|
||||||
|
template(#prepend)
|
||||||
|
i.las.la-envelope
|
||||||
|
q-btn.full-width.q-mt-sm(
|
||||||
|
type='submit'
|
||||||
|
push
|
||||||
|
color='primary'
|
||||||
|
:label='t(`auth.sendResetPassword`)'
|
||||||
|
no-caps
|
||||||
|
icon='las la-life-ring'
|
||||||
|
)
|
||||||
|
q-separator.q-my-md
|
||||||
|
q-btn.acrylic-btn.full-width(
|
||||||
|
flat
|
||||||
|
color='primary'
|
||||||
|
:label='t(`auth.forgotPasswordCancel`)'
|
||||||
|
no-caps
|
||||||
|
icon='las la-arrow-circle-left'
|
||||||
|
@click='switchTo(`login`)'
|
||||||
|
)
|
||||||
|
|
||||||
|
//- -----------------------------------------------------
|
||||||
|
//- REGISTER SCREEN
|
||||||
|
//- -----------------------------------------------------
|
||||||
|
template(v-else-if='state.screen === `register`')
|
||||||
|
p {{t('auth.registerSubTitle')}}
|
||||||
|
q-form(ref='registerForm', @submit='register')
|
||||||
|
q-input(
|
||||||
|
ref='registerNameIpt'
|
||||||
|
v-model='state.newName'
|
||||||
|
outlined
|
||||||
|
:rules='userNameValidation'
|
||||||
|
lazy-rules='ondemand'
|
||||||
|
hide-bottom-space
|
||||||
|
:label='t(`auth.fields.name`)'
|
||||||
|
autocomplete='name'
|
||||||
|
)
|
||||||
|
template(#prepend)
|
||||||
|
i.las.la-user-circle
|
||||||
|
q-input.q-mt-sm(
|
||||||
|
type='email'
|
||||||
|
v-model='state.newEmail'
|
||||||
|
outlined
|
||||||
|
:rules='userEmailValidation'
|
||||||
|
lazy-rules='ondemand'
|
||||||
|
hide-bottom-space
|
||||||
|
:label='t(`auth.fields.email`)'
|
||||||
|
autocomplete='email'
|
||||||
|
)
|
||||||
|
template(#prepend)
|
||||||
|
i.las.la-envelope
|
||||||
|
q-input.q-mt-sm(
|
||||||
|
v-model='state.newPassword'
|
||||||
|
outlined
|
||||||
|
:label='t(`auth.fields.password`)'
|
||||||
|
type='password'
|
||||||
|
autocomplete='new-password'
|
||||||
|
:rules='userPasswordValidation'
|
||||||
|
hide-bottom-space
|
||||||
|
lazy-rules='ondemand'
|
||||||
|
)
|
||||||
|
template(#append)
|
||||||
|
q-badge(
|
||||||
|
v-show='state.newPassword'
|
||||||
|
:color='passwordStrength.color'
|
||||||
|
:label='passwordStrength.label'
|
||||||
|
)
|
||||||
|
template(#prepend)
|
||||||
|
i.las.la-key
|
||||||
|
q-input.q-mt-sm(
|
||||||
|
v-model='state.newPasswordVerify'
|
||||||
|
outlined
|
||||||
|
:label='t(`auth.fields.verifyPassword`)'
|
||||||
|
type='password'
|
||||||
|
autocomplete='new-password'
|
||||||
|
:rules='userPasswordVerifyValidation'
|
||||||
|
hide-bottom-space
|
||||||
|
lazy-rules='ondemand'
|
||||||
|
)
|
||||||
|
template(#prepend)
|
||||||
|
i.las.la-key
|
||||||
|
q-btn.full-width.q-mt-sm(
|
||||||
|
type='submit'
|
||||||
|
push
|
||||||
|
color='primary'
|
||||||
|
:label='t(`auth.actions.register`)'
|
||||||
|
no-caps
|
||||||
|
icon='las la-user-plus'
|
||||||
|
)
|
||||||
|
q-separator.q-my-md
|
||||||
|
q-btn.acrylic-btn.full-width(
|
||||||
|
flat
|
||||||
|
color='primary'
|
||||||
|
:label='t(`auth.switchToLogin.link`)'
|
||||||
|
no-caps
|
||||||
|
icon='las la-arrow-circle-left'
|
||||||
|
@click='switchTo(`login`)'
|
||||||
|
)
|
||||||
|
|
||||||
|
//- -----------------------------------------------------
|
||||||
|
//- CHANGE PASSWORD SCREEN
|
||||||
|
//- -----------------------------------------------------
|
||||||
|
template(v-else-if='state.screen === `changePwd`')
|
||||||
|
p(v-if='state.continuationToken') {{t('auth.changePwd.instructions')}}
|
||||||
|
q-form(ref='changePwdForm', @submit='changePwd')
|
||||||
|
q-input(
|
||||||
|
v-if='!state.continuationToken'
|
||||||
|
ref='changePwdCurrentIpt'
|
||||||
|
v-model='state.password'
|
||||||
|
outlined
|
||||||
|
type='password'
|
||||||
|
:rules='loginPasswordValidation'
|
||||||
|
lazy-rules='ondemand'
|
||||||
|
hide-bottom-space
|
||||||
|
:label='t(`auth.changePwd.currentPassword`)'
|
||||||
|
autocomplete='password'
|
||||||
|
)
|
||||||
|
template(#prepend)
|
||||||
|
i.las.la-key
|
||||||
|
q-input.q-mt-sm(
|
||||||
|
ref='changePwdNewPwdIpt'
|
||||||
|
v-model='state.newPassword'
|
||||||
|
outlined
|
||||||
|
:label='t(`auth.changePwd.newPassword`)'
|
||||||
|
type='password'
|
||||||
|
autocomplete='new-password'
|
||||||
|
:rules='userPasswordValidation'
|
||||||
|
hide-bottom-space
|
||||||
|
lazy-rules='ondemand'
|
||||||
|
)
|
||||||
|
template(#append)
|
||||||
|
q-badge(
|
||||||
|
v-show='state.newPassword'
|
||||||
|
:color='passwordStrength.color'
|
||||||
|
:label='passwordStrength.label'
|
||||||
|
)
|
||||||
|
template(#prepend)
|
||||||
|
i.las.la-key
|
||||||
|
q-input.q-mt-sm(
|
||||||
|
v-model='state.newPasswordVerify'
|
||||||
|
outlined
|
||||||
|
:label='t(`auth.changePwd.newPasswordVerify`)'
|
||||||
|
type='password'
|
||||||
|
autocomplete='new-password'
|
||||||
|
:rules='userPasswordVerifyValidation'
|
||||||
|
hide-bottom-space
|
||||||
|
lazy-rules='ondemand'
|
||||||
|
)
|
||||||
|
template(#prepend)
|
||||||
|
i.las.la-key
|
||||||
|
q-btn.full-width.q-mt-sm(
|
||||||
|
type='submit'
|
||||||
|
push
|
||||||
|
color='primary'
|
||||||
|
:label='t(`auth.changePwd.proceed`)'
|
||||||
|
no-caps
|
||||||
|
icon='las la-sync-alt'
|
||||||
|
)
|
||||||
|
//- -----------------------------------------------------
|
||||||
|
//- TFA SCREEN
|
||||||
|
//- -----------------------------------------------------
|
||||||
|
template(v-else-if='state.screen === `tfa`')
|
||||||
|
p {{t('auth.tfa.subtitle')}}
|
||||||
|
.auth-login-tfa
|
||||||
|
v-otp-input(
|
||||||
|
ref='tfaIpt'
|
||||||
|
:num-inputs='6'
|
||||||
|
:should-auto-focus='true'
|
||||||
|
input-classes='otp-input'
|
||||||
|
input-type='number'
|
||||||
|
separator=''
|
||||||
|
@on-change='v => state.securityCode = v'
|
||||||
|
@on-complete='verifyTFA'
|
||||||
|
)
|
||||||
|
q-btn.full-width.q-mt-md(
|
||||||
|
push
|
||||||
|
color='primary'
|
||||||
|
:label='t(`auth.tfa.verifyToken`)'
|
||||||
|
no-caps
|
||||||
|
icon='las la-sign-in-alt'
|
||||||
|
@click='verifyTFA'
|
||||||
|
)
|
||||||
|
//- -----------------------------------------------------
|
||||||
|
//- TFA SETUP SCREEN
|
||||||
|
//- -----------------------------------------------------
|
||||||
|
template(v-else-if='state.screen === `tfasetup`')
|
||||||
|
p TODO - TFA Setup not available yet.
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import gql from 'graphql-tag'
|
||||||
|
import { find } from 'lodash-es'
|
||||||
|
import Cookies from 'js-cookie'
|
||||||
|
import zxcvbn from 'zxcvbn'
|
||||||
|
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useQuasar } from 'quasar'
|
||||||
|
import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue'
|
||||||
|
|
||||||
|
import { useSiteStore } from 'src/stores/site'
|
||||||
|
import { useUserStore } from 'src/stores/user'
|
||||||
|
|
||||||
|
import VOtpInput from 'vue3-otp-input'
|
||||||
|
|
||||||
|
// QUASAR
|
||||||
|
|
||||||
|
const $q = useQuasar()
|
||||||
|
|
||||||
|
// STORES
|
||||||
|
|
||||||
|
const siteStore = useSiteStore()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
// I18N
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
// DATA
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
strategies: [],
|
||||||
|
selectedStrategyId: null,
|
||||||
|
screen: 'login',
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
securityCode: '',
|
||||||
|
continuationToken: '',
|
||||||
|
newName: '',
|
||||||
|
newEmail: '',
|
||||||
|
newPassword: '',
|
||||||
|
newPasswordVerify: '',
|
||||||
|
isTFAShown: false,
|
||||||
|
isTFASetupShown: false,
|
||||||
|
tfaQRImage: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// REFS
|
||||||
|
|
||||||
|
const loginEmailIpt = ref(null)
|
||||||
|
const forgotEmailIpt = ref(null)
|
||||||
|
const registerNameIpt = ref(null)
|
||||||
|
const changePwdCurrentIpt = ref(null)
|
||||||
|
const changePwdNewPwdIpt = ref(null)
|
||||||
|
const loginForm = ref(null)
|
||||||
|
const forgotForm = ref(null)
|
||||||
|
const registerForm = ref(null)
|
||||||
|
const changePwdForm = ref(null)
|
||||||
|
|
||||||
|
// COMPUTED
|
||||||
|
|
||||||
|
const selectedStrategy = computed(() => {
|
||||||
|
return (state.selectedStrategyId && find(state.strategies, ['id', state.selectedStrategyId])) || {}
|
||||||
|
})
|
||||||
|
|
||||||
|
const passwordStrength = computed(() => {
|
||||||
|
if (state.newPassword.length < 8) {
|
||||||
|
return {
|
||||||
|
color: 'negative',
|
||||||
|
label: t('common.password.weak')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch (zxcvbn(state.newPassword).score) {
|
||||||
|
case 1:
|
||||||
|
return {
|
||||||
|
color: 'deep-orange-7',
|
||||||
|
label: t('common.password.poor')
|
||||||
|
}
|
||||||
|
case 2:
|
||||||
|
return {
|
||||||
|
color: 'purple-7',
|
||||||
|
label: t('common.password.average')
|
||||||
|
}
|
||||||
|
case 3:
|
||||||
|
return {
|
||||||
|
color: 'blue-7',
|
||||||
|
label: t('common.password.good')
|
||||||
|
}
|
||||||
|
case 4:
|
||||||
|
return {
|
||||||
|
color: 'green-7',
|
||||||
|
label: t('common.password.strong')
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
color: 'negative',
|
||||||
|
label: t('common.password.weak')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// VALIDATION RULES
|
||||||
|
|
||||||
|
const loginUsernameValidation = [
|
||||||
|
val => val.length > 0 || t('auth.errors.missingUsername')
|
||||||
|
]
|
||||||
|
|
||||||
|
const loginPasswordValidation = [
|
||||||
|
val => val.length > 0 || t('auth.errors.missingPassword')
|
||||||
|
]
|
||||||
|
|
||||||
|
const userNameValidation = [
|
||||||
|
val => val.length > 0 || t('auth.errors.missingName'),
|
||||||
|
val => /^[^<>"]+$/.test(val) || t('auth.errors.invalidName')
|
||||||
|
]
|
||||||
|
|
||||||
|
const userEmailValidation = [
|
||||||
|
val => val.length > 0 || t('auth.errors.missingEmail'),
|
||||||
|
val => /^.+@.+\..+$/.test(val) || t('auth.errors.invalidEmail')
|
||||||
|
]
|
||||||
|
|
||||||
|
const userPasswordValidation = [
|
||||||
|
val => val.length > 0 || t('auth.errors.missingPassword'),
|
||||||
|
val => val.length >= 8 || t('auth.errors.passwordTooShort')
|
||||||
|
]
|
||||||
|
|
||||||
|
const userPasswordVerifyValidation = [
|
||||||
|
val => val.length > 0 || t('auth.errors.missingVerifyPassword'),
|
||||||
|
val => val === state.newPassword || t('auth.errors.passwordsNotMatch')
|
||||||
|
]
|
||||||
|
|
||||||
|
// METHODS
|
||||||
|
|
||||||
|
function switchTo (screen) {
|
||||||
|
switch (screen) {
|
||||||
|
case 'login': {
|
||||||
|
state.screen = 'login'
|
||||||
|
nextTick(() => {
|
||||||
|
loginEmailIpt.value.focus()
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'forgot': {
|
||||||
|
state.screen = 'forgot'
|
||||||
|
nextTick(() => {
|
||||||
|
forgotEmailIpt.value.focus()
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'register': {
|
||||||
|
state.screen = 'register'
|
||||||
|
nextTick(() => {
|
||||||
|
registerNameIpt.value.focus()
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
throw new Error('Invalid Screen')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchStrategies (showAll = false) {
|
||||||
|
const resp = await APOLLO_CLIENT.query({
|
||||||
|
query: gql`
|
||||||
|
query loginFetchSiteStrategies(
|
||||||
|
$siteId: UUID!
|
||||||
|
$visibleOnly: Boolean
|
||||||
|
) {
|
||||||
|
authSiteStrategies(
|
||||||
|
siteId: $siteId
|
||||||
|
visibleOnly: $visibleOnly
|
||||||
|
) {
|
||||||
|
id
|
||||||
|
activeStrategy {
|
||||||
|
id
|
||||||
|
displayName
|
||||||
|
strategy {
|
||||||
|
key
|
||||||
|
color
|
||||||
|
icon
|
||||||
|
useForm
|
||||||
|
usernameType
|
||||||
|
}
|
||||||
|
selfRegistration
|
||||||
|
}
|
||||||
|
order
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
variables: {
|
||||||
|
siteId: siteStore.id,
|
||||||
|
visibleOnly: !showAll
|
||||||
|
}
|
||||||
|
})
|
||||||
|
state.strategies = resp.data?.authSiteStrategies ?? []
|
||||||
|
state.selectedStrategyId = state.strategies[0].id
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleLoginResponse (resp) {
|
||||||
|
state.continuationToken = resp.continuationToken
|
||||||
|
if (resp.mustChangePwd === true) {
|
||||||
|
state.screen = 'changePwd'
|
||||||
|
nextTick(() => {
|
||||||
|
if (state.continuationToken) {
|
||||||
|
changePwdNewPwdIpt.value.focus()
|
||||||
|
} else {
|
||||||
|
changePwdCurrentIpt.value.focus()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
$q.loading.hide()
|
||||||
|
} else if (resp.mustProvideTFA === true) {
|
||||||
|
state.securityCode = ''
|
||||||
|
state.screen = 'tfa'
|
||||||
|
$q.loading.hide()
|
||||||
|
} else if (resp.mustSetupTFA === true) {
|
||||||
|
state.securityCode = ''
|
||||||
|
state.screen = 'tfasetup'
|
||||||
|
state.tfaQRImage = resp.tfaQRImage
|
||||||
|
nextTick(() => {
|
||||||
|
this.$refs.iptTFASetup.focus()
|
||||||
|
})
|
||||||
|
$q.loading.hide()
|
||||||
|
} else {
|
||||||
|
$q.loading.show({
|
||||||
|
message: t('auth.loginSuccess'),
|
||||||
|
backgroundColor: 'green'
|
||||||
|
})
|
||||||
|
Cookies.set('jwt', resp.jwt, { expires: 365 })
|
||||||
|
setTimeout(() => {
|
||||||
|
const loginRedirect = Cookies.get('loginRedirect')
|
||||||
|
if (loginRedirect === '/' && resp.redirect) {
|
||||||
|
Cookies.remove('loginRedirect')
|
||||||
|
window.location.replace(resp.redirect)
|
||||||
|
} else if (loginRedirect) {
|
||||||
|
Cookies.remove('loginRedirect')
|
||||||
|
window.location.replace(loginRedirect)
|
||||||
|
} else if (resp.redirect) {
|
||||||
|
window.location.replace(resp.redirect)
|
||||||
|
} else {
|
||||||
|
window.location.replace('/')
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LOGIN
|
||||||
|
*/
|
||||||
|
async function login () {
|
||||||
|
$q.loading.show({
|
||||||
|
message: t('auth.signingIn')
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
const isFormValid = await loginForm.value.validate(true)
|
||||||
|
if (!isFormValid) {
|
||||||
|
throw new Error(t('auth.errors.login'))
|
||||||
|
}
|
||||||
|
const resp = await APOLLO_CLIENT.mutate({
|
||||||
|
mutation: gql`
|
||||||
|
mutation(
|
||||||
|
$username: String!
|
||||||
|
$password: String!
|
||||||
|
$strategyId: UUID!
|
||||||
|
$siteId: UUID
|
||||||
|
) {
|
||||||
|
login(
|
||||||
|
username: $username
|
||||||
|
password: $password
|
||||||
|
strategyId: $strategyId
|
||||||
|
siteId: $siteId
|
||||||
|
) {
|
||||||
|
operation {
|
||||||
|
succeeded
|
||||||
|
message
|
||||||
|
}
|
||||||
|
jwt
|
||||||
|
mustChangePwd
|
||||||
|
mustProvideTFA
|
||||||
|
mustSetupTFA
|
||||||
|
continuationToken
|
||||||
|
redirect
|
||||||
|
tfaQRImage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
variables: {
|
||||||
|
username: state.username,
|
||||||
|
password: state.password,
|
||||||
|
strategyId: state.selectedStrategyId,
|
||||||
|
siteId: siteStore.id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (resp.data?.login?.operation?.succeeded) {
|
||||||
|
state.password = ''
|
||||||
|
await handleLoginResponse(resp.data.login)
|
||||||
|
} else {
|
||||||
|
throw new Error(resp.data?.login?.operation?.message || t('auth.errors.loginError'))
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
$q.loading.hide()
|
||||||
|
$q.notify({
|
||||||
|
type: 'negative',
|
||||||
|
message: err.message
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FORGOT PASSWORD
|
||||||
|
*/
|
||||||
|
async function forgotPassword () {
|
||||||
|
try {
|
||||||
|
const isFormValid = await forgotForm.value.validate(true)
|
||||||
|
if (!isFormValid) {
|
||||||
|
throw new Error(t('auth.errors.forgotPassword'))
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
$q.notify({
|
||||||
|
type: 'negative',
|
||||||
|
message: err.message
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* REGISTER
|
||||||
|
*/
|
||||||
|
async function register () {
|
||||||
|
try {
|
||||||
|
const isFormValid = await registerForm.value.validate(true)
|
||||||
|
if (!isFormValid) {
|
||||||
|
throw new Error(t('auth.errors.register'))
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
$q.notify({
|
||||||
|
type: 'negative',
|
||||||
|
message: err.message
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CHANGE PASSWORD
|
||||||
|
*/
|
||||||
|
async function changePwd () {
|
||||||
|
try {
|
||||||
|
const isFormValid = await changePwdForm.value.validate(true)
|
||||||
|
if (!isFormValid) {
|
||||||
|
throw new Error(t('auth.errors.register'))
|
||||||
|
}
|
||||||
|
const resp = await APOLLO_CLIENT.mutate({
|
||||||
|
mutation: gql`
|
||||||
|
mutation (
|
||||||
|
$userId: UUID!
|
||||||
|
$continuationToken: String
|
||||||
|
$currentPassword: String
|
||||||
|
$newPassword: String!
|
||||||
|
$strategyId: UUID!
|
||||||
|
$siteId: UUID
|
||||||
|
) {
|
||||||
|
changePassword (
|
||||||
|
userId: $userId
|
||||||
|
continuationToken: $continuationToken
|
||||||
|
currentPassword: $currentPassword
|
||||||
|
newPassword: $newPassword
|
||||||
|
strategyId: $strategyId
|
||||||
|
siteId: $siteId
|
||||||
|
) {
|
||||||
|
operation {
|
||||||
|
succeeded
|
||||||
|
message
|
||||||
|
}
|
||||||
|
jwt
|
||||||
|
mustChangePwd
|
||||||
|
mustProvideTFA
|
||||||
|
mustSetupTFA
|
||||||
|
continuationToken
|
||||||
|
redirect
|
||||||
|
tfaQRImage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
variables: {
|
||||||
|
userId: userStore.id,
|
||||||
|
continuationToken: state.continuationToken,
|
||||||
|
currentPassword: state.password,
|
||||||
|
newPassword: state.newPassword,
|
||||||
|
strategyId: state.selectedStrategyId,
|
||||||
|
siteId: siteStore.id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (resp.data?.login?.operation?.succeeded) {
|
||||||
|
state.password = ''
|
||||||
|
await handleLoginResponse(resp.data.login)
|
||||||
|
} else {
|
||||||
|
throw new Error(resp.data?.login?.operation?.message || t('auth.errors.loginError'))
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
$q.notify({
|
||||||
|
type: 'negative',
|
||||||
|
message: err.message
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VERIFY TFA TOKEN
|
||||||
|
*/
|
||||||
|
async function verifyTFA () {
|
||||||
|
$q.loading.show({
|
||||||
|
message: t('auth.signingIn')
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
if (!/^[0-9]{6}$/.test(state.securityCode)) {
|
||||||
|
throw new Error(t('auth.errors.tfaMissing'))
|
||||||
|
}
|
||||||
|
const resp = await APOLLO_CLIENT.mutate({
|
||||||
|
mutation: gql`
|
||||||
|
mutation(
|
||||||
|
continuationToken: String!
|
||||||
|
securityCode: String!
|
||||||
|
) {
|
||||||
|
loginTFA(
|
||||||
|
continuationToken: $continuationToken
|
||||||
|
securityCode: $securityCode
|
||||||
|
) {
|
||||||
|
operation {
|
||||||
|
succeeded
|
||||||
|
message
|
||||||
|
}
|
||||||
|
jwt
|
||||||
|
mustChangePwd
|
||||||
|
mustProvideTFA
|
||||||
|
mustSetupTFA
|
||||||
|
continuationToken
|
||||||
|
redirect
|
||||||
|
tfaQRImage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
variables: {
|
||||||
|
continuationToken: state.continuationToken,
|
||||||
|
securityCode: state.securityCode
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (resp.data?.login?.operation?.succeeded) {
|
||||||
|
state.continuationToken = ''
|
||||||
|
state.securityCode = ''
|
||||||
|
await handleLoginResponse(resp.data.loginTFA)
|
||||||
|
} else {
|
||||||
|
throw new Error(resp.data?.loginTFA?.operation?.message || t('auth.errors.loginError'))
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
$q.loading.hide()
|
||||||
|
$q.notify({
|
||||||
|
type: 'negative',
|
||||||
|
message: err.message
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MOUNTED
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await fetchStrategies()
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.auth-login-tfa {
|
||||||
|
> div {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.otp-input {
|
||||||
|
width: 100%;
|
||||||
|
height: 48px;
|
||||||
|
padding: 5px;
|
||||||
|
margin: 0 5px;
|
||||||
|
font-size: 20px;
|
||||||
|
border-radius: 6px;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
@at-root .body--light & {
|
||||||
|
border: 2px solid rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@at-root .body--dark & {
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline-color: $primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Background colour of an input field with value */
|
||||||
|
&.is-complete {
|
||||||
|
border-color: $positive;
|
||||||
|
border-width: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-inner-spin-button,
|
||||||
|
&::-webkit-outer-spin-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in new issue