Skip to content

Commit c644fda

Browse files
committed
feat: add authetification via password/biometric/passkey
1 parent bd33628 commit c644fda

26 files changed

+835
-97
lines changed

android/app/capacitor.build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
1111
dependencies {
1212
implementation project(':capacitor-community-file-opener')
1313
implementation project(':capacitor-filesystem')
14+
implementation project(':capgo-capacitor-native-biometric')
1415

1516
}
1617

android/app/src/main/AndroidManifest.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,5 @@
3535
<uses-permission android:name="android.permission.CAMERA" />
3636
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
3737
<uses-permission android:name="android.permission.VIBRATE" />
38+
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
3839
</manifest>

android/capacitor.settings.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,6 @@ project(':capacitor-community-file-opener').projectDir = new File('../node_modul
77

88
include ':capacitor-filesystem'
99
project(':capacitor-filesystem').projectDir = new File('../node_modules/@capacitor/filesystem/android')
10+
11+
include ':capgo-capacitor-native-biometric'
12+
project(':capgo-capacitor-native-biometric').projectDir = new File('../node_modules/@capgo/capacitor-native-biometric/android')

package-lock.json

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
"@capacitor/android": "^7.3.0",
4444
"@capacitor/core": "^7.3.0",
4545
"@capacitor/filesystem": "^7.1.1",
46+
"@capgo/capacitor-native-biometric": "^7.1.13",
4647
"@emoji-mart/data": "^1.2.1",
4748
"@klayr/codec": "^0.5.1",
4849
"@klayr/cryptography": "^4.1.1",
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
<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>
8+
9+
<v-btn v-if="showRetryButton" class="a-btn-primary" @click="authenticate">
10+
{{ t('biometric_login.try_again') }}
11+
</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>
21+
</v-col>
22+
</v-row>
23+
</div>
24+
</template>
25+
26+
<script lang="ts" setup>
27+
import { computed, ref, onMounted } from 'vue'
28+
import { useI18n } from 'vue-i18n'
29+
import { biometricAuth } from '@/lib/auth'
30+
import { AuthenticationResult } from '@/lib/auth/types'
31+
import { useStore } from 'vuex'
32+
33+
const { t } = useI18n()
34+
const store = useStore()
35+
36+
const emit = defineEmits<{
37+
(e: 'login'): void
38+
(e: 'error', error: string): void
39+
}>()
40+
41+
const isAuthenticating = ref(false)
42+
const showRetryButton = ref(false)
43+
const hasError = ref(false)
44+
45+
const iconColor = computed(() => {
46+
if (isAuthenticating.value) return 'primary'
47+
if (hasError.value) return 'error'
48+
return 'primary'
49+
})
50+
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+
57+
const handleAuthError = (errorMessage: string) => {
58+
showRetryButton.value = true
59+
hasError.value = true
60+
emit('error', errorMessage)
61+
}
62+
63+
const authenticate = async () => {
64+
isAuthenticating.value = true
65+
showRetryButton.value = false
66+
hasError.value = false
67+
68+
try {
69+
const biometricResult = await biometricAuth.authorizeUser()
70+
71+
if (biometricResult === AuthenticationResult.Cancel) {
72+
showRetryButton.value = true
73+
return
74+
}
75+
76+
if (biometricResult !== AuthenticationResult.Success) {
77+
handleAuthError(t('biometric_login.authentication_failed'))
78+
return
79+
}
80+
81+
await store.dispatch('loginViaAuthentication')
82+
83+
emit('login')
84+
} catch (error) {
85+
handleAuthError(error instanceof Error ? error.message : t('biometric_login.authentication_failed'))
86+
} finally {
87+
isAuthenticating.value = false
88+
}
89+
}
90+
91+
const switchToPassphrase = () => {
92+
store.commit('options/updateOption', { key: 'stayLoggedIn', value: false })
93+
store.commit('options/updateOption', { key: 'authenticationMethod', value: null })
94+
}
95+
96+
onMounted(() => {
97+
authenticate()
98+
})
99+
</script>
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
<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>
8+
9+
<v-btn v-if="showRetryButton" class="a-btn-primary" @click="authenticate">
10+
{{ t('passkey_login.try_again') }}
11+
</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>
21+
</v-col>
22+
</v-row>
23+
</div>
24+
</template>
25+
26+
<script lang="ts" setup>
27+
import { computed, ref, onMounted } from 'vue'
28+
import { useI18n } from 'vue-i18n'
29+
import { passkeyAuth } from '@/lib/auth'
30+
import { AuthenticationResult } from '@/lib/auth/types'
31+
import { useStore } from 'vuex'
32+
33+
const { t } = useI18n()
34+
const store = useStore()
35+
36+
const emit = defineEmits<{
37+
(e: 'login'): void
38+
(e: 'error', error: string): void
39+
}>()
40+
41+
const isAuthenticating = ref(false)
42+
const showRetryButton = ref(false)
43+
const hasError = ref(false)
44+
45+
const iconColor = computed(() => {
46+
if (isAuthenticating.value) return 'primary'
47+
if (hasError.value) return 'error'
48+
return 'primary'
49+
})
50+
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+
57+
const handleAuthError = (errorMessage: string) => {
58+
showRetryButton.value = true
59+
hasError.value = true
60+
emit('error', errorMessage)
61+
}
62+
63+
const authenticate = async () => {
64+
isAuthenticating.value = true
65+
showRetryButton.value = false
66+
hasError.value = false
67+
68+
try {
69+
const passkeyResult = await passkeyAuth.authorizeUser()
70+
71+
if (passkeyResult === AuthenticationResult.Cancel) {
72+
showRetryButton.value = true
73+
return
74+
}
75+
76+
if (passkeyResult !== AuthenticationResult.Success) {
77+
handleAuthError(t('passkey_login.authentication_failed'))
78+
return
79+
}
80+
81+
await store.dispatch('loginViaAuthentication')
82+
83+
emit('login')
84+
} catch (error) {
85+
handleAuthError(error instanceof Error ? error.message : t('passkey_login.authentication_failed'))
86+
} finally {
87+
isAuthenticating.value = false
88+
}
89+
}
90+
91+
const switchToPassphrase = () => {
92+
store.commit('options/updateOption', { key: 'stayLoggedIn', value: false })
93+
store.commit('options/updateOption', { key: 'authenticationMethod', value: null })
94+
}
95+
96+
onMounted(() => {
97+
authenticate()
98+
})
99+
</script>

src/components/LoginForm.vue renamed to src/components/auth/PassphraseLoginForm.vue

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
<v-form ref="form" :class="classes.root" @submit.prevent="submit">
33
<v-row no-gutters>
44
<slot>
5-
<!-- Todo: check src/components/PasswordSetDialog.vue component and consider the possibility to move common code to new component -->
65
<v-text-field
76
ref="passphraseInput"
87
v-model="passphrase"
@@ -63,7 +62,6 @@ import { isAxiosError } from 'axios'
6362
import { isAllNodesOfflineError, isAllNodesDisabledError } from '@/lib/nodes/utils/errors'
6463
import { mdiEye, mdiEyeOff } from '@mdi/js'
6564
import { useSaveCursor } from '@/hooks/useSaveCursor'
66-
import { NodeStatusResult } from '@/lib/nodes/abstract.node'
6765
6866
const className = 'login-form'
6967
const classes = {

src/components/LoginPasswordForm.vue renamed to src/components/auth/PasswordLoginForm.vue

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ import { mdiEye, mdiEyeOff } from '@mdi/js'
6060
import { useSaveCursor } from '@/hooks/useSaveCursor'
6161
import { useConsiderOffline } from '@/hooks/useConsiderOffline'
6262
import { NodeStatusResult } from '@/lib/nodes/abstract.node'
63+
import { passwordAuth } from '@/lib/auth'
6364
6465
const className = 'login-form'
6566
const classes = {
@@ -106,8 +107,8 @@ const admNodesDisabled = computed(() => admNodes.value.some((node) => node.statu
106107
const submit = () => {
107108
showSpinner.value = true
108109
109-
return store
110-
.dispatch('loginViaPassword', password.value)
110+
return passwordAuth
111+
.authorizeUser(password.value)
111112
.then(() => {
112113
emit('login')
113114
})

0 commit comments

Comments
 (0)