|
@@ -51,6 +51,16 @@
|
|
no-caps
|
|
no-caps
|
|
icon='las la-sign-in-alt'
|
|
icon='las la-sign-in-alt'
|
|
)
|
|
)
|
|
|
|
+ template(v-if='canUsePasskeys')
|
|
|
|
+ q-separator.q-my-md
|
|
|
|
+ q-btn.acrylic-btn.full-width(
|
|
|
|
+ flat
|
|
|
|
+ color='primary'
|
|
|
|
+ :label='t(`auth.passkeys.signin`)'
|
|
|
|
+ no-caps
|
|
|
|
+ icon='las la-key'
|
|
|
|
+ @click='switchTo(`passkey`)'
|
|
|
|
+ )
|
|
template(v-if='selectedStrategy.activeStrategy?.strategy?.key === `local`')
|
|
template(v-if='selectedStrategy.activeStrategy?.strategy?.key === `local`')
|
|
q-separator.q-my-md
|
|
q-separator.q-my-md
|
|
q-btn.acrylic-btn.full-width.q-mb-sm(
|
|
q-btn.acrylic-btn.full-width.q-mb-sm(
|
|
@@ -71,6 +81,40 @@
|
|
@click='switchTo(`forgot`)'
|
|
@click='switchTo(`forgot`)'
|
|
)
|
|
)
|
|
|
|
|
|
|
|
+ //- -----------------------------------------------------
|
|
|
|
+ //- PASSKEY LOGIN SCREEN
|
|
|
|
+ //- -----------------------------------------------------
|
|
|
|
+ template(v-else-if='state.screen === `passkey`')
|
|
|
|
+ p {{t('auth.passkeys.signinHint')}}
|
|
|
|
+ q-form(ref='passkeyForm', @submit='loginWithPasskey')
|
|
|
|
+ q-input(
|
|
|
|
+ ref='passkeyEmailIpt'
|
|
|
|
+ v-model='state.username'
|
|
|
|
+ outlined
|
|
|
|
+ hide-bottom-space
|
|
|
|
+ :label='t(`auth.fields.email`)'
|
|
|
|
+ autocomplete='webauthn'
|
|
|
|
+ )
|
|
|
|
+ template(#prepend)
|
|
|
|
+ i.las.la-envelope
|
|
|
|
+ q-btn.full-width.q-mt-sm(
|
|
|
|
+ type='submit'
|
|
|
|
+ push
|
|
|
|
+ color='primary'
|
|
|
|
+ :label='t(`auth.actions.login`)'
|
|
|
|
+ no-caps
|
|
|
|
+ icon='las la-key'
|
|
|
|
+ )
|
|
|
|
+ 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`)'
|
|
|
|
+ )
|
|
|
|
+
|
|
//- -----------------------------------------------------
|
|
//- -----------------------------------------------------
|
|
//- FORGOT PASSWORD SCREEN
|
|
//- FORGOT PASSWORD SCREEN
|
|
//- -----------------------------------------------------
|
|
//- -----------------------------------------------------
|
|
@@ -298,10 +342,14 @@ import gql from 'graphql-tag'
|
|
import { find } from 'lodash-es'
|
|
import { find } from 'lodash-es'
|
|
import Cookies from 'js-cookie'
|
|
import Cookies from 'js-cookie'
|
|
import zxcvbn from 'zxcvbn'
|
|
import zxcvbn from 'zxcvbn'
|
|
-
|
|
|
|
import { useI18n } from 'vue-i18n'
|
|
import { useI18n } from 'vue-i18n'
|
|
import { useQuasar } from 'quasar'
|
|
import { useQuasar } from 'quasar'
|
|
import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue'
|
|
import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue'
|
|
|
|
+import {
|
|
|
|
+ browserSupportsWebAuthn,
|
|
|
|
+ browserSupportsWebAuthnAutofill,
|
|
|
|
+ startAuthentication
|
|
|
|
+} from '@simplewebauthn/browser'
|
|
|
|
|
|
import { useSiteStore } from 'src/stores/site'
|
|
import { useSiteStore } from 'src/stores/site'
|
|
import { useUserStore } from 'src/stores/user'
|
|
import { useUserStore } from 'src/stores/user'
|
|
@@ -343,6 +391,7 @@ const state = reactive({
|
|
// REFS
|
|
// REFS
|
|
|
|
|
|
const loginEmailIpt = ref(null)
|
|
const loginEmailIpt = ref(null)
|
|
|
|
+const passkeyEmailIpt = ref(null)
|
|
const forgotEmailIpt = ref(null)
|
|
const forgotEmailIpt = ref(null)
|
|
const registerNameIpt = ref(null)
|
|
const registerNameIpt = ref(null)
|
|
const changePwdCurrentIpt = ref(null)
|
|
const changePwdCurrentIpt = ref(null)
|
|
@@ -395,6 +444,10 @@ const passwordStrength = computed(() => {
|
|
}
|
|
}
|
|
})
|
|
})
|
|
|
|
|
|
|
|
+const canUsePasskeys = computed(() => {
|
|
|
|
+ return browserSupportsWebAuthn()
|
|
|
|
+})
|
|
|
|
+
|
|
// VALIDATION RULES
|
|
// VALIDATION RULES
|
|
|
|
|
|
const loginUsernameValidation = [
|
|
const loginUsernameValidation = [
|
|
@@ -436,6 +489,13 @@ function switchTo (screen) {
|
|
})
|
|
})
|
|
break
|
|
break
|
|
}
|
|
}
|
|
|
|
+ case 'passkey': {
|
|
|
|
+ state.screen = 'passkey'
|
|
|
|
+ nextTick(() => {
|
|
|
|
+ passkeyEmailIpt.value.focus()
|
|
|
|
+ })
|
|
|
|
+ break
|
|
|
|
+ }
|
|
case 'forgot': {
|
|
case 'forgot': {
|
|
state.screen = 'forgot'
|
|
state.screen = 'forgot'
|
|
nextTick(() => {
|
|
nextTick(() => {
|
|
@@ -598,7 +658,7 @@ async function login () {
|
|
})
|
|
})
|
|
if (resp.data?.login?.operation?.succeeded) {
|
|
if (resp.data?.login?.operation?.succeeded) {
|
|
state.password = ''
|
|
state.password = ''
|
|
- await handleLoginResponse(resp.data.login)
|
|
|
|
|
|
+ handleLoginResponse(resp.data.login)
|
|
} else {
|
|
} else {
|
|
throw new Error(resp.data?.login?.operation?.message || t('auth.errors.loginError'))
|
|
throw new Error(resp.data?.login?.operation?.message || t('auth.errors.loginError'))
|
|
}
|
|
}
|
|
@@ -611,6 +671,81 @@ async function login () {
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+/**
|
|
|
|
+ * LOGIN WITH PASSKEY
|
|
|
|
+ */
|
|
|
|
+async function loginWithPasskey () {
|
|
|
|
+ $q.loading.show({
|
|
|
|
+ message: t('auth.signingIn')
|
|
|
|
+ })
|
|
|
|
+ try {
|
|
|
|
+ const respGen = await APOLLO_CLIENT.mutate({
|
|
|
|
+ mutation: gql`
|
|
|
|
+ mutation authenticatePasskeyGenerate (
|
|
|
|
+ $email: String!
|
|
|
|
+ $siteId: UUID!
|
|
|
|
+ ) {
|
|
|
|
+ authenticatePasskeyGenerate (
|
|
|
|
+ email: $email
|
|
|
|
+ siteId: $siteId
|
|
|
|
+ ) {
|
|
|
|
+ operation {
|
|
|
|
+ succeeded
|
|
|
|
+ message
|
|
|
|
+ }
|
|
|
|
+ authOptions
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ `,
|
|
|
|
+ variables: {
|
|
|
|
+ email: state.username,
|
|
|
|
+ siteId: siteStore.id
|
|
|
|
+ }
|
|
|
|
+ })
|
|
|
|
+ if (respGen.data?.authenticatePasskeyGenerate?.operation?.succeeded) {
|
|
|
|
+ const authResp = await startAuthentication(respGen.data.authenticatePasskeyGenerate.authOptions, await browserSupportsWebAuthnAutofill())
|
|
|
|
+
|
|
|
|
+ const respVerif = await APOLLO_CLIENT.mutate({
|
|
|
|
+ mutation: gql`
|
|
|
|
+ mutation authenticatePasskeyVerify (
|
|
|
|
+ $authResponse: JSON!
|
|
|
|
+ ) {
|
|
|
|
+ authenticatePasskeyVerify (
|
|
|
|
+ authResponse: $authResponse
|
|
|
|
+ ) {
|
|
|
|
+ operation {
|
|
|
|
+ succeeded
|
|
|
|
+ message
|
|
|
|
+ }
|
|
|
|
+ jwt
|
|
|
|
+ nextAction
|
|
|
|
+ continuationToken
|
|
|
|
+ redirect
|
|
|
|
+ tfaQRImage
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ `,
|
|
|
|
+ variables: {
|
|
|
|
+ authResponse: authResp
|
|
|
|
+ }
|
|
|
|
+ })
|
|
|
|
+ if (respVerif.data?.authenticatePasskeyVerify?.operation?.succeeded) {
|
|
|
|
+ handleLoginResponse(respVerif.data.authenticatePasskeyVerify)
|
|
|
|
+ } else {
|
|
|
|
+ throw new Error(respVerif.data?.authenticatePasskeyVerify?.operation?.message || t('auth.errors.loginError'))
|
|
|
|
+ }
|
|
|
|
+ } else {
|
|
|
|
+ throw new Error(respGen.data?.authenticatePasskeyGenerate?.operation?.message || t('auth.errors.loginError'))
|
|
|
|
+ }
|
|
|
|
+ } catch (err) {
|
|
|
|
+ $q.loading.hide()
|
|
|
|
+ $q.notify({
|
|
|
|
+ type: 'negative',
|
|
|
|
+ message: err.message
|
|
|
|
+ })
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
/**
|
|
/**
|
|
* FORGOT PASSWORD
|
|
* FORGOT PASSWORD
|
|
*/
|
|
*/
|