Skip to content

Commit 00e88b6

Browse files
committed
feat: improvements and fixes authentification methods
1 parent b446ba1 commit 00e88b6

File tree

15 files changed

+460
-256
lines changed

15 files changed

+460
-256
lines changed

src/assets/styles/components/_login-form.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
@use 'sass:map';
22
@use '../settings/_colors.scss';
3+
@use '@/assets/styles/generic/_variables.scss';
34

45
/**
56
* Centering input text and label.
@@ -13,6 +14,9 @@
1314
* 5. Disable border bottom animation.
1415
*/
1516
.login-form {
17+
@media (max-width: map.get(variables.$breakpoints, 'mobile')) {
18+
margin-bottom: 40px;
19+
}
1620
&__button {
1721
min-width: 126px;
1822
}
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
<template>
2+
<v-dialog v-model="show" width="320">
3+
<v-card>
4+
<v-card-title class="a-text-header">
5+
{{ t('login.signin_options.title') }}
6+
</v-card-title>
7+
8+
<v-divider class="a-divider" />
9+
10+
<v-card-text class="pa-0">
11+
<v-list>
12+
<v-list-item
13+
avatar
14+
:disabled="!biometricOption.available"
15+
@click="selectAuthMethod(AuthenticationMethod.Biometric)"
16+
>
17+
<template #prepend>
18+
<v-icon :icon="mdiFingerprint" />
19+
</template>
20+
21+
<v-list-item-title>{{ t('login.signin_options.biometric.title') }}</v-list-item-title>
22+
<v-list-item-subtitle>{{ biometricOption.subtitle }}</v-list-item-subtitle>
23+
</v-list-item>
24+
25+
<v-list-item
26+
avatar
27+
:disabled="!passkeyOption.available"
28+
@click="selectAuthMethod(AuthenticationMethod.Passkey)"
29+
>
30+
<template #prepend>
31+
<v-icon :icon="mdiKeyVariant" />
32+
</template>
33+
34+
<v-list-item-title>{{ t('login.signin_options.passkey.title') }}</v-list-item-title>
35+
<v-list-item-subtitle>{{ passkeyOption.subtitle }}</v-list-item-subtitle>
36+
</v-list-item>
37+
38+
<v-list-item
39+
avatar
40+
:disabled="!passwordOption.available"
41+
@click="selectAuthMethod(AuthenticationMethod.Password)"
42+
>
43+
<template #prepend>
44+
<v-icon :icon="mdiLock" />
45+
</template>
46+
47+
<v-list-item-title>{{ t('login.signin_options.password.title') }}</v-list-item-title>
48+
<v-list-item-subtitle>{{ passwordOption.subtitle }}</v-list-item-subtitle>
49+
</v-list-item>
50+
</v-list>
51+
</v-card-text>
52+
</v-card>
53+
</v-dialog>
54+
</template>
55+
56+
<script setup lang="ts">
57+
import { computed, ref, onMounted } from 'vue'
58+
import { useI18n } from 'vue-i18n'
59+
import { Capacitor } from '@capacitor/core'
60+
import { mdiFingerprint, mdiKeyVariant, mdiLock } from '@mdi/js'
61+
62+
import { AuthenticationMethod } from '@/lib/auth/types'
63+
import { db as isIDBSupported } from '@/lib/idb'
64+
65+
const props = defineProps({
66+
modelValue: {
67+
type: Boolean,
68+
required: true
69+
}
70+
})
71+
72+
const emit = defineEmits<{
73+
(e: 'update:modelValue', value: boolean): void
74+
(e: 'select-auth-method', method: AuthenticationMethod): void
75+
}>()
76+
77+
const { t } = useI18n()
78+
79+
const show = computed({
80+
get() {
81+
return props.modelValue
82+
},
83+
set(value) {
84+
emit('update:modelValue', value)
85+
}
86+
})
87+
88+
const biometricOption = computed(() => {
89+
const isNative = Capacitor.isNativePlatform()
90+
91+
if (!isNative) {
92+
return {
93+
available: false,
94+
subtitle: t('login.signin_options.biometric.only_native')
95+
}
96+
}
97+
98+
return {
99+
available: true,
100+
subtitle: t('login.signin_options.biometric.device_touchid_faceid')
101+
}
102+
})
103+
104+
const passkeyOption = computed(() => {
105+
const isNative = Capacitor.isNativePlatform()
106+
const hasWebAuthn = !!(navigator.credentials && window.PublicKeyCredential)
107+
108+
if (isNative) {
109+
return {
110+
available: false,
111+
subtitle: t('login.signin_options.passkey.only_web')
112+
}
113+
}
114+
115+
if (!hasWebAuthn) {
116+
return {
117+
available: false,
118+
subtitle: t('login.signin_options.not_available')
119+
}
120+
}
121+
122+
return {
123+
available: true,
124+
subtitle: t('login.signin_options.passkey.secure_login')
125+
}
126+
})
127+
128+
const isPasswordAvailable = ref(true)
129+
130+
const passwordOption = computed(() => {
131+
return {
132+
available: isPasswordAvailable.value,
133+
subtitle: isPasswordAvailable.value
134+
? t('login.signin_options.password.secure_login')
135+
: t('login.signin_options.not_available')
136+
}
137+
})
138+
139+
onMounted(async () => {
140+
try {
141+
await isIDBSupported
142+
isPasswordAvailable.value = true
143+
} catch {
144+
isPasswordAvailable.value = false
145+
}
146+
})
147+
148+
const selectAuthMethod = (method: AuthenticationMethod) => {
149+
emit('select-auth-method', method)
150+
show.value = false
151+
}
152+
</script>
153+
154+
<style scoped>
155+
:deep(.v-list-item-subtitle) {
156+
display: block;
157+
}
158+
</style>

src/components/auth/BiometricLoginForm.vue

Lines changed: 29 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,40 @@
11
<template>
2-
<div class="biometric-login-form">
3-
<v-row align="center" justify="center" no-gutters>
4-
<v-col cols="12" class="text-center">
5-
<v-icon icon="mdi-fingerprint" size="64" :color="iconColor" class="mb-4" />
6-
7-
<h3 class="mb-4">{{ statusText }}</h3>
2+
<v-form @submit.prevent="authenticate">
3+
<v-row no-gutters>
4+
<div class="text-center" style="width: 100%; padding: 32px 32px 16px">
5+
<v-icon :icon="mdiFingerprint" size="48" :color="iconColor" @click="authenticate" />
6+
</div>
7+
</v-row>
88

9-
<v-btn v-if="showRetryButton" class="a-btn-primary" @click="authenticate">
10-
{{ t('biometric_login.try_again') }}
9+
<v-row align="center" justify="center" class="mt-2" no-gutters>
10+
<v-col cols="12">
11+
<v-btn class="a-btn-primary" @click="authenticate" :disabled="isAuthenticating">
12+
<v-progress-circular
13+
v-show="isAuthenticating"
14+
indeterminate
15+
color="primary"
16+
size="24"
17+
class="mr-4"
18+
/>
19+
{{ t('login_via_password.user_password_unlock') }}
1120
</v-btn>
12-
13-
<div class="text-center mt-6">
14-
<h3 class="a-text-regular">
15-
{{ t('biometric_login.use_passphrase_hint') }}
16-
</h3>
17-
<v-btn class="a-btn-link mt-2" variant="text" size="small" @click="switchToPassphrase">
18-
{{ t('biometric_login.use_passphrase') }}
19-
</v-btn>
20-
</div>
2121
</v-col>
22+
<div class="text-center mt-11">
23+
<h3 class="a-text-regular">
24+
{{ t('login.use_passphrase_hint') }}
25+
</h3>
26+
<v-btn class="a-btn-link mt-2" variant="text" size="small" @click="switchToPassphrase">
27+
{{ t('login.use_passphrase') }}
28+
</v-btn>
29+
</div>
2230
</v-row>
23-
</div>
31+
</v-form>
2432
</template>
2533

2634
<script lang="ts" setup>
2735
import { computed, ref, onMounted } from 'vue'
2836
import { useI18n } from 'vue-i18n'
37+
import { mdiFingerprint } from '@mdi/js'
2938
import { biometricAuth } from '@/lib/auth'
3039
import { AuthenticationResult } from '@/lib/auth/types'
3140
import { useStore } from 'vuex'
@@ -39,7 +48,6 @@ const emit = defineEmits<{
3948
}>()
4049
4150
const isAuthenticating = ref(false)
42-
const showRetryButton = ref(false)
4351
const hasError = ref(false)
4452
4553
const iconColor = computed(() => {
@@ -48,41 +56,32 @@ const iconColor = computed(() => {
4856
return 'primary'
4957
})
5058
51-
const statusText = computed(() => {
52-
if (isAuthenticating.value) return t('biometric_login.authenticating')
53-
if (hasError.value) return t('biometric_login.authentication_failed')
54-
return t('biometric_login.touch_sensor')
55-
})
56-
5759
const handleAuthError = (errorMessage: string) => {
58-
showRetryButton.value = true
5960
hasError.value = true
6061
emit('error', errorMessage)
6162
}
6263
6364
const authenticate = async () => {
6465
isAuthenticating.value = true
65-
showRetryButton.value = false
6666
hasError.value = false
6767
6868
try {
6969
const biometricResult = await biometricAuth.authorizeUser()
7070
7171
if (biometricResult === AuthenticationResult.Cancel) {
72-
showRetryButton.value = true
7372
return
7473
}
7574
7675
if (biometricResult !== AuthenticationResult.Success) {
77-
handleAuthError(t('biometric_login.authentication_failed'))
76+
handleAuthError(t('login.authentication_failed'))
7877
return
7978
}
8079
8180
await store.dispatch('loginViaAuthentication')
8281
8382
emit('login')
8483
} catch (error) {
85-
handleAuthError(error instanceof Error ? error.message : t('biometric_login.authentication_failed'))
84+
handleAuthError(error instanceof Error ? error.message : t('login.authentication_failed'))
8685
} finally {
8786
isAuthenticating.value = false
8887
}

src/components/auth/PasskeyLoginForm.vue

Lines changed: 29 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,40 @@
11
<template>
2-
<div class="passkey-login-form">
3-
<v-row align="center" justify="center" no-gutters>
4-
<v-col cols="12" class="text-center">
5-
<v-icon icon="mdi-key" size="64" :color="iconColor" class="mb-4" />
6-
7-
<h3 class="mb-4">{{ statusText }}</h3>
2+
<v-form @submit.prevent="authenticate">
3+
<v-row no-gutters>
4+
<div class="text-center" style="width: 100%; padding: 32px 32px 16px">
5+
<v-icon :icon="mdiKeyVariant" size="48" :color="iconColor" @click="authenticate" />
6+
</div>
7+
</v-row>
88

9-
<v-btn v-if="showRetryButton" class="a-btn-primary" @click="authenticate">
10-
{{ t('passkey_login.try_again') }}
9+
<v-row align="center" justify="center" class="mt-2" no-gutters>
10+
<v-col cols="12">
11+
<v-btn class="a-btn-primary" @click="authenticate" :disabled="isAuthenticating">
12+
<v-progress-circular
13+
v-show="isAuthenticating"
14+
indeterminate
15+
color="primary"
16+
size="24"
17+
class="mr-4"
18+
/>
19+
{{ t('login_via_password.user_password_unlock') }}
1120
</v-btn>
12-
13-
<div class="text-center mt-6">
14-
<h3 class="a-text-regular">
15-
{{ t('passkey_login.use_passphrase_hint') }}
16-
</h3>
17-
<v-btn class="a-btn-link mt-2" variant="text" size="small" @click="switchToPassphrase">
18-
{{ t('passkey_login.use_passphrase') }}
19-
</v-btn>
20-
</div>
2121
</v-col>
22+
<div class="text-center mt-11">
23+
<h3 class="a-text-regular">
24+
{{ t('login.use_passphrase_hint') }}
25+
</h3>
26+
<v-btn class="a-btn-link mt-2" variant="text" size="small" @click="switchToPassphrase">
27+
{{ t('login.use_passphrase') }}
28+
</v-btn>
29+
</div>
2230
</v-row>
23-
</div>
31+
</v-form>
2432
</template>
2533

2634
<script lang="ts" setup>
2735
import { computed, ref, onMounted } from 'vue'
2836
import { useI18n } from 'vue-i18n'
37+
import { mdiKeyVariant } from '@mdi/js'
2938
import { passkeyAuth } from '@/lib/auth'
3039
import { AuthenticationResult } from '@/lib/auth/types'
3140
import { useStore } from 'vuex'
@@ -39,7 +48,6 @@ const emit = defineEmits<{
3948
}>()
4049
4150
const isAuthenticating = ref(false)
42-
const showRetryButton = ref(false)
4351
const hasError = ref(false)
4452
4553
const iconColor = computed(() => {
@@ -48,41 +56,32 @@ const iconColor = computed(() => {
4856
return 'primary'
4957
})
5058
51-
const statusText = computed(() => {
52-
if (isAuthenticating.value) return t('passkey_login.authenticating')
53-
if (hasError.value) return t('passkey_login.authentication_failed')
54-
return t('passkey_login.touch_sensor')
55-
})
56-
5759
const handleAuthError = (errorMessage: string) => {
58-
showRetryButton.value = true
5960
hasError.value = true
6061
emit('error', errorMessage)
6162
}
6263
6364
const authenticate = async () => {
6465
isAuthenticating.value = true
65-
showRetryButton.value = false
6666
hasError.value = false
6767
6868
try {
6969
const passkeyResult = await passkeyAuth.authorizeUser()
7070
7171
if (passkeyResult === AuthenticationResult.Cancel) {
72-
showRetryButton.value = true
7372
return
7473
}
7574
7675
if (passkeyResult !== AuthenticationResult.Success) {
77-
handleAuthError(t('passkey_login.authentication_failed'))
76+
handleAuthError(t('login.authentication_failed'))
7877
return
7978
}
8079
8180
await store.dispatch('loginViaAuthentication')
8281
8382
emit('login')
8483
} catch (error) {
85-
handleAuthError(error instanceof Error ? error.message : t('passkey_login.authentication_failed'))
84+
handleAuthError(error instanceof Error ? error.message : t('login.authentication_failed'))
8685
} finally {
8786
isAuthenticating.value = false
8887
}

0 commit comments

Comments
 (0)