mirror of
https://github.com/mayswind/ezbookkeeping.git
synced 2026-05-20 17:54:30 +08:00
migrate app lock settings page to composition API and typescript
This commit is contained in:
@@ -27,7 +27,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue';
|
import { ref, computed, watch } from 'vue';
|
||||||
|
|
||||||
import { useI18n } from '@/locales/helpers.ts';
|
import { useI18n } from '@/locales/helpers.ts';
|
||||||
|
|
||||||
@@ -74,6 +74,12 @@ function onSheetOpen(): void {
|
|||||||
function onSheetClosed(): void {
|
function onSheetClosed(): void {
|
||||||
cancel();
|
cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(() => props.modelValue, (newValue) => {
|
||||||
|
if (newValue === '') {
|
||||||
|
currentPinCode.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
+1
-5
@@ -230,11 +230,7 @@ export function getTextAfter(fullText: string, text: string): string {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function base64encode(arrayBuffer: ArrayBuffer): string | null {
|
export function base64encode(arrayBuffer: ArrayBuffer): string {
|
||||||
if (!arrayBuffer) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return btoa(String.fromCharCode.apply(null, Array.from(new Uint8Array(arrayBuffer))));
|
return btoa(String.fromCharCode.apply(null, Array.from(new Uint8Array(arrayBuffer))));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export interface Framework7Dom {
|
|||||||
|
|
||||||
type TranslateFunction = (message: string) => string;
|
type TranslateFunction = (message: string) => string;
|
||||||
|
|
||||||
export function showAlert(message: string, confirmCallback: (dialog: Dialog.Dialog, e: Event) => void, translateFn: TranslateFunction): void {
|
export function showAlert(message: string, confirmCallback: ((dialog: Dialog.Dialog, e: Event) => void) | undefined, translateFn: TranslateFunction): void {
|
||||||
f7ready((f7) => {
|
f7ready((f7) => {
|
||||||
f7.dialog.create({
|
f7.dialog.create({
|
||||||
title: translateFn('global.app.title'),
|
title: translateFn('global.app.title'),
|
||||||
@@ -229,7 +229,7 @@ export function useI18nUIComponents() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
showAlert: (message: string, confirmCallback: (dialog: Dialog.Dialog, e: Event) => void) => showAlert(message, confirmCallback, i18nGlobal.t),
|
showAlert: (message: string, confirmCallback?: (dialog: Dialog.Dialog, e: Event) => void) => showAlert(message, confirmCallback, i18nGlobal.t),
|
||||||
showConfirm: (message: string, confirmCallback: (dialog: Dialog.Dialog, e: Event) => void, cancelCallback?: (dialog: Dialog.Dialog, e: Event) => void): void => showConfirm(message, confirmCallback, cancelCallback, i18nGlobal.t),
|
showConfirm: (message: string, confirmCallback: (dialog: Dialog.Dialog, e: Event) => void, cancelCallback?: (dialog: Dialog.Dialog, e: Event) => void): void => showConfirm(message, confirmCallback, cancelCallback, i18nGlobal.t),
|
||||||
showToast: (message: string, timeout?: number): void => showToast(message, timeout, i18nGlobal.t),
|
showToast: (message: string, timeout?: number): void => showToast(message, timeout, i18nGlobal.t),
|
||||||
routeBackOnError
|
routeBackOnError
|
||||||
|
|||||||
+2
-2
@@ -28,7 +28,7 @@ interface AttestationData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface WebAuthnRegisterResponse {
|
interface WebAuthnRegisterResponse {
|
||||||
readonly id: string | null;
|
readonly id: string;
|
||||||
readonly clientData: ClientData;
|
readonly clientData: ClientData;
|
||||||
readonly publicKey: Uint8Array | null;
|
readonly publicKey: Uint8Array | null;
|
||||||
readonly rawCredential: Credential;
|
readonly rawCredential: Credential;
|
||||||
@@ -198,7 +198,7 @@ export function verifyWebAuthnCredential(userInfo: UserBasicInfo, credentialId:
|
|||||||
clientData && clientData.type === 'webauthn.get' && challengeFromClientData === challenge &&
|
clientData && clientData.type === 'webauthn.get' && challengeFromClientData === challenge &&
|
||||||
userIdParts && userIdParts.length === 2 && userIdParts[0] === userInfo.username) {
|
userIdParts && userIdParts.length === 2 && userIdParts[0] === userInfo.username) {
|
||||||
const ret: WebAuthnVerifyResponse = {
|
const ret: WebAuthnVerifyResponse = {
|
||||||
id: base64encode(rawCredential.rawId) as string,
|
id: base64encode(rawCredential.rawId),
|
||||||
userName: userIdParts[0],
|
userName: userIdParts[0],
|
||||||
userSecret: userIdParts[1],
|
userSecret: userIdParts[1],
|
||||||
clientData: clientData,
|
clientData: clientData,
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { ref, computed } from 'vue';
|
||||||
|
|
||||||
|
import { useSettingsStore } from '@/stores/setting.ts';
|
||||||
|
|
||||||
|
import { isWebAuthnCompletelySupported } from '@/lib/webauthn.ts';
|
||||||
|
|
||||||
|
export function useAppLockPageBase() {
|
||||||
|
const settingsStore = useSettingsStore();
|
||||||
|
|
||||||
|
const isSupportedWebAuthn = ref<boolean>(false);
|
||||||
|
|
||||||
|
const isEnableApplicationLock = computed<boolean>({
|
||||||
|
get: () => settingsStore.appSettings.applicationLock,
|
||||||
|
set: (value) => settingsStore.setEnableApplicationLock(value)
|
||||||
|
});
|
||||||
|
|
||||||
|
const isEnableApplicationLockWebAuthn = computed<boolean>({
|
||||||
|
get: () => settingsStore.appSettings.applicationLockWebAuthn,
|
||||||
|
set: (value) => settingsStore.setEnableApplicationLockWebAuthn(value)
|
||||||
|
});
|
||||||
|
|
||||||
|
isWebAuthnCompletelySupported().then(result => {
|
||||||
|
isSupportedWebAuthn.value = result;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
// states
|
||||||
|
isSupportedWebAuthn,
|
||||||
|
// computed states
|
||||||
|
isEnableApplicationLock,
|
||||||
|
isEnableApplicationLockWebAuthn
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,31 +1,32 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-row>
|
<v-row>
|
||||||
<v-col cols="12">
|
<v-col cols="12">
|
||||||
<v-card :title="$t('Application Lock')">
|
<v-card :title="tt('Application Lock')">
|
||||||
<v-card-text class="pb-0">
|
<v-card-text class="pb-0">
|
||||||
<p class="text-body-1 font-weight-semibold" v-if="!isEnableApplicationLock">
|
<p class="text-body-1 font-weight-semibold" v-if="!isEnableApplicationLock">
|
||||||
{{ $t('Application lock is not enabled') }}
|
{{ tt('Application lock is not enabled') }}
|
||||||
</p>
|
</p>
|
||||||
<p class="text-body-1" v-if="isEnableApplicationLock">
|
<p class="text-body-1" v-if="isEnableApplicationLock">
|
||||||
{{ $t('Application lock has been enabled') }}
|
{{ tt('Application lock has been enabled') }}
|
||||||
</p>
|
</p>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
|
|
||||||
<v-card-text v-if="isEnableApplicationLock">
|
<v-card-text v-if="isEnableApplicationLock">
|
||||||
<v-switch :disabled="true"
|
<v-switch :disabled="true"
|
||||||
:label="$t('Unlock with PIN Code')"
|
:label="tt('Unlock with PIN Code')"
|
||||||
v-model="isEnableApplicationLock"/>
|
v-model="isEnableApplicationLock"/>
|
||||||
<v-switch :label="$t('Unlock with WebAuthn')"
|
<v-switch :label="tt('Unlock with WebAuthn')"
|
||||||
:loading="enablingWebAuthn"
|
:loading="enablingWebAuthn"
|
||||||
v-model="isEnableApplicationLockWebAuthn"/>
|
v-model="isEnableApplicationLockWebAuthn"
|
||||||
|
v-if="isSupportedWebAuthn"/>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
|
|
||||||
<v-card-text class="pb-0">
|
<v-card-text class="pb-0">
|
||||||
<p class="text-body-1 font-weight-semibold" v-if="!isEnableApplicationLock">
|
<p class="text-body-1 font-weight-semibold" v-if="!isEnableApplicationLock">
|
||||||
{{ $t('Please enter a new 6-digit PIN code. The PIN code would encrypt your local data, so you need to enter it every time you open this app. If this PIN code is lost, you will need to log in again.') }}
|
{{ tt('Please enter a new 6-digit PIN code. The PIN code would encrypt your local data, so you need to enter it every time you open this app. If this PIN code is lost, you will need to log in again.') }}
|
||||||
</p>
|
</p>
|
||||||
<p class="text-body-1 font-weight-semibold" v-if="isEnableApplicationLock">
|
<p class="text-body-1 font-weight-semibold" v-if="isEnableApplicationLock">
|
||||||
{{ $t('Your current PIN code is required to disable application lock.') }}
|
{{ tt('Your current PIN code is required to disable application lock.') }}
|
||||||
</p>
|
</p>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
|
|
||||||
@@ -44,11 +45,11 @@
|
|||||||
<v-col cols="12" class="d-flex flex-wrap gap-4">
|
<v-col cols="12" class="d-flex flex-wrap gap-4">
|
||||||
<v-btn :disabled="!pinCodeValid"
|
<v-btn :disabled="!pinCodeValid"
|
||||||
v-if="!isEnableApplicationLock" @click="enable">
|
v-if="!isEnableApplicationLock" @click="enable">
|
||||||
{{ $t('Enable Application Lock') }}
|
{{ tt('Enable Application Lock') }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn :disabled="!pinCodeValid"
|
<v-btn :disabled="!pinCodeValid"
|
||||||
v-if="isEnableApplicationLock" @click="disable">
|
v-if="isEnableApplicationLock" @click="disable">
|
||||||
{{ $t('Disable Application Lock') }}
|
{{ tt('Disable Application Lock') }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
@@ -60,16 +61,19 @@
|
|||||||
<snack-bar ref="snackbar" />
|
<snack-bar ref="snackbar" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import { mapStores } from 'pinia';
|
import SnackBar from '@/components/desktop/SnackBar.vue';
|
||||||
|
|
||||||
|
import { ref, computed, useTemplateRef, watch } from 'vue';
|
||||||
|
|
||||||
|
import { useI18n } from '@/locales/helpers.ts';
|
||||||
|
import { useAppLockPageBase } from '@/views/base/settings/AppLockPageBase.ts';
|
||||||
|
|
||||||
import { useSettingsStore } from '@/stores/setting.ts';
|
import { useSettingsStore } from '@/stores/setting.ts';
|
||||||
import { useUserStore } from '@/stores/user.ts';
|
import { useUserStore } from '@/stores/user.ts';
|
||||||
import { useTransactionsStore } from '@/stores/transaction.js';
|
import { useTransactionsStore } from '@/stores/transaction.js';
|
||||||
|
|
||||||
import {
|
import { registerWebAuthnCredential } from '@/lib/webauthn.ts';
|
||||||
isWebAuthnCompletelySupported,
|
|
||||||
registerWebAuthnCredential
|
|
||||||
} from '@/lib/webauthn.ts';
|
|
||||||
import {
|
import {
|
||||||
getUserAppLockState,
|
getUserAppLockState,
|
||||||
encryptToken,
|
encryptToken,
|
||||||
@@ -80,141 +84,122 @@ import {
|
|||||||
} from '@/lib/userstate.ts';
|
} from '@/lib/userstate.ts';
|
||||||
import logger from '@/lib/logger.ts';
|
import logger from '@/lib/logger.ts';
|
||||||
|
|
||||||
export default {
|
type SnackBarType = InstanceType<typeof SnackBar>;
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
isSupportedWebAuthn: false,
|
|
||||||
enablingWebAuthn: false,
|
|
||||||
pinCode: ''
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapStores(useSettingsStore, useUserStore, useTransactionsStore),
|
|
||||||
isEnableApplicationLock: {
|
|
||||||
get: function () {
|
|
||||||
return this.settingsStore.appSettings.applicationLock;
|
|
||||||
},
|
|
||||||
set: function (value) {
|
|
||||||
this.settingsStore.setEnableApplicationLock(value);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
isEnableApplicationLockWebAuthn: {
|
|
||||||
get: function () {
|
|
||||||
return this.settingsStore.appSettings.applicationLockWebAuthn;
|
|
||||||
},
|
|
||||||
set: function (value) {
|
|
||||||
this.settingsStore.setEnableApplicationLockWebAuthn(value);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
pinCodeValid() {
|
|
||||||
return this.pinCode && this.pinCode.length === 6;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
isEnableApplicationLockWebAuthn: function (newValue) {
|
|
||||||
const self = this;
|
|
||||||
|
|
||||||
if (newValue) {
|
const { tt } = useI18n();
|
||||||
self.enablingWebAuthn = true;
|
|
||||||
|
|
||||||
registerWebAuthnCredential(
|
const { isSupportedWebAuthn, isEnableApplicationLock, isEnableApplicationLockWebAuthn } = useAppLockPageBase();
|
||||||
getUserAppLockState(),
|
|
||||||
self.userStore.currentUserBasicInfo,
|
|
||||||
).then(({ id }) => {
|
|
||||||
self.enablingWebAuthn = false;
|
|
||||||
|
|
||||||
saveWebAuthnConfig(id);
|
const settingsStore = useSettingsStore();
|
||||||
self.settingsStore.setEnableApplicationLockWebAuthn(true);
|
const userStore = useUserStore();
|
||||||
self.$refs.snackbar.showMessage('You have enabled WebAuthn successfully');
|
|
||||||
}).catch(error => {
|
|
||||||
logger.error('failed to enable WebAuthn', error);
|
|
||||||
|
|
||||||
self.enablingWebAuthn = false;
|
const snackbar = useTemplateRef<SnackBarType>('snackbar');
|
||||||
|
|
||||||
if (error.notSupported) {
|
const pinCode = ref<string>('');
|
||||||
self.$refs.snackbar.showMessage('WebAuth is not supported on this device');
|
const enablingWebAuthn = ref<boolean>(false);
|
||||||
} else if (error.name === 'NotAllowedError') {
|
const transactionsStore = useTransactionsStore();
|
||||||
self.$refs.snackbar.showMessage('User has canceled authentication');
|
|
||||||
} else if (error.invalid) {
|
|
||||||
self.$refs.snackbar.showMessage('Failed to enable WebAuthn');
|
|
||||||
} else {
|
|
||||||
self.$refs.snackbar.showMessage('User has canceled or this device does not support WebAuthn');
|
|
||||||
}
|
|
||||||
|
|
||||||
self.isEnableApplicationLockWebAuthn = false;
|
const pinCodeValid = computed<boolean>(() => {
|
||||||
self.settingsStore.setEnableApplicationLockWebAuthn(false);
|
return pinCode.value?.length === 6 || false;
|
||||||
clearWebAuthnConfig();
|
});
|
||||||
});
|
|
||||||
} else {
|
|
||||||
self.settingsStore.setEnableApplicationLockWebAuthn(false);
|
|
||||||
clearWebAuthnConfig();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created() {
|
|
||||||
const self = this;
|
|
||||||
isWebAuthnCompletelySupported().then(result => {
|
|
||||||
self.isSupportedWebAuthn = result;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
confirm() {
|
|
||||||
if (this.isEnableApplicationLock) {
|
|
||||||
this.disable();
|
|
||||||
} else {
|
|
||||||
this.enable();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
enable() {
|
|
||||||
if (this.settingsStore.appSettings.applicationLock) {
|
|
||||||
this.$refs.snackbar.showMessage('Application lock has been enabled');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.pinCode || this.pinCode.length !== 6) {
|
function confirm(): void {
|
||||||
this.pinCode = '';
|
if (isEnableApplicationLock.value) {
|
||||||
this.$refs.snackbar.showMessage('Invalid PIN code');
|
disable();
|
||||||
return;
|
} else {
|
||||||
}
|
enable();
|
||||||
|
|
||||||
const user = this.userStore.currentUserBasicInfo;
|
|
||||||
|
|
||||||
if (!user || !user.username) {
|
|
||||||
this.pinCode = '';
|
|
||||||
this.$refs.snackbar.showMessage('An error occurred');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
encryptToken(user.username, this.pinCode);
|
|
||||||
this.settingsStore.setEnableApplicationLock(true);
|
|
||||||
this.transactionsStore.saveTransactionDraft();
|
|
||||||
|
|
||||||
this.settingsStore.setEnableApplicationLockWebAuthn(false);
|
|
||||||
clearWebAuthnConfig();
|
|
||||||
|
|
||||||
this.pinCode = '';
|
|
||||||
},
|
|
||||||
disable() {
|
|
||||||
if (!this.settingsStore.appSettings.applicationLock) {
|
|
||||||
this.$refs.snackbar.showMessage('Application lock is not enabled');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isCorrectPinCode(this.pinCode)) {
|
|
||||||
this.pinCode = '';
|
|
||||||
this.$refs.snackbar.showMessage('Incorrect PIN code');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.pinCode = '';
|
|
||||||
|
|
||||||
decryptToken();
|
|
||||||
this.settingsStore.setEnableApplicationLock(false);
|
|
||||||
this.transactionsStore.saveTransactionDraft();
|
|
||||||
|
|
||||||
this.settingsStore.setEnableApplicationLockWebAuthn(false);
|
|
||||||
clearWebAuthnConfig();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function enable(): void {
|
||||||
|
if (settingsStore.appSettings.applicationLock) {
|
||||||
|
snackbar.value?.showMessage('Application lock has been enabled');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pinCode.value || pinCode.value.length !== 6) {
|
||||||
|
pinCode.value = '';
|
||||||
|
snackbar.value?.showMessage('Invalid PIN code');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = userStore.currentUserBasicInfo;
|
||||||
|
|
||||||
|
if (!user || !user.username) {
|
||||||
|
pinCode.value = '';
|
||||||
|
snackbar.value?.showMessage('An error occurred');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
encryptToken(user.username, pinCode.value);
|
||||||
|
settingsStore.setEnableApplicationLock(true);
|
||||||
|
transactionsStore.saveTransactionDraft();
|
||||||
|
|
||||||
|
settingsStore.setEnableApplicationLockWebAuthn(false);
|
||||||
|
clearWebAuthnConfig();
|
||||||
|
|
||||||
|
pinCode.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function disable(): void {
|
||||||
|
if (!settingsStore.appSettings.applicationLock) {
|
||||||
|
snackbar.value?.showMessage('Application lock is not enabled');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isCorrectPinCode(pinCode.value)) {
|
||||||
|
pinCode.value = '';
|
||||||
|
snackbar.value?.showMessage('Incorrect PIN code');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pinCode.value = '';
|
||||||
|
|
||||||
|
decryptToken();
|
||||||
|
settingsStore.setEnableApplicationLock(false);
|
||||||
|
transactionsStore.saveTransactionDraft();
|
||||||
|
|
||||||
|
settingsStore.setEnableApplicationLockWebAuthn(false);
|
||||||
|
clearWebAuthnConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(isEnableApplicationLockWebAuthn, (newValue) => {
|
||||||
|
const userAppLockState = getUserAppLockState();
|
||||||
|
|
||||||
|
if (newValue && userAppLockState && userStore.currentUserBasicInfo) {
|
||||||
|
enablingWebAuthn.value = true;
|
||||||
|
|
||||||
|
registerWebAuthnCredential(
|
||||||
|
userAppLockState,
|
||||||
|
userStore.currentUserBasicInfo,
|
||||||
|
).then(({ id }) => {
|
||||||
|
enablingWebAuthn.value = false;
|
||||||
|
|
||||||
|
saveWebAuthnConfig(id);
|
||||||
|
settingsStore.setEnableApplicationLockWebAuthn(true);
|
||||||
|
snackbar.value?.showMessage('You have enabled WebAuthn successfully');
|
||||||
|
}).catch(error => {
|
||||||
|
logger.error('failed to enable WebAuthn', error);
|
||||||
|
|
||||||
|
enablingWebAuthn.value = false;
|
||||||
|
|
||||||
|
if (error.notSupported) {
|
||||||
|
snackbar.value?.showMessage('WebAuth is not supported on this device');
|
||||||
|
} else if (error.name === 'NotAllowedError') {
|
||||||
|
snackbar.value?.showMessage('User has canceled authentication');
|
||||||
|
} else if (error.invalid) {
|
||||||
|
snackbar.value?.showMessage('Failed to enable WebAuthn');
|
||||||
|
} else {
|
||||||
|
snackbar.value?.showMessage('User has canceled or this device does not support WebAuthn');
|
||||||
|
}
|
||||||
|
|
||||||
|
isEnableApplicationLockWebAuthn.value = false;
|
||||||
|
settingsStore.setEnableApplicationLockWebAuthn(false);
|
||||||
|
clearWebAuthnConfig();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
settingsStore.setEnableApplicationLockWebAuthn(false);
|
||||||
|
clearWebAuthnConfig();
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,33 +1,33 @@
|
|||||||
<template>
|
<template>
|
||||||
<f7-page>
|
<f7-page>
|
||||||
<f7-navbar>
|
<f7-navbar>
|
||||||
<f7-nav-left :back-link="$t('Back')"></f7-nav-left>
|
<f7-nav-left :back-link="tt('Back')"></f7-nav-left>
|
||||||
<f7-nav-title :title="$t('Application Lock')"></f7-nav-title>
|
<f7-nav-title :title="tt('Application Lock')"></f7-nav-title>
|
||||||
</f7-navbar>
|
</f7-navbar>
|
||||||
|
|
||||||
<f7-list strong inset dividers class="margin-top">
|
<f7-list strong inset dividers class="margin-top">
|
||||||
<f7-list-item :title="$t('Status')" :after="$t(isEnableApplicationLock ? 'Enabled' : 'Disabled')"></f7-list-item>
|
<f7-list-item :title="tt('Status')" :after="tt(isEnableApplicationLock ? 'Enabled' : 'Disabled')"></f7-list-item>
|
||||||
<f7-list-item v-if="isEnableApplicationLock">
|
<f7-list-item v-if="isEnableApplicationLock">
|
||||||
<span>{{ $t('Unlock with PIN Code') }}</span>
|
<span>{{ tt('Unlock with PIN Code') }}</span>
|
||||||
<f7-toggle checked disabled></f7-toggle>
|
<f7-toggle checked disabled></f7-toggle>
|
||||||
</f7-list-item>
|
</f7-list-item>
|
||||||
<f7-list-item v-if="isEnableApplicationLock && isSupportedWebAuthn">
|
<f7-list-item v-if="isEnableApplicationLock && isSupportedWebAuthn">
|
||||||
<span>{{ $t('Unlock with WebAuthn') }}</span>
|
<span>{{ tt('Unlock with WebAuthn') }}</span>
|
||||||
<f7-toggle :checked="isEnableApplicationLockWebAuthn" @toggle:change="isEnableApplicationLockWebAuthn = $event"></f7-toggle>
|
<f7-toggle :checked="isEnableApplicationLockWebAuthn" @toggle:change="isEnableApplicationLockWebAuthn = $event"></f7-toggle>
|
||||||
</f7-list-item>
|
</f7-list-item>
|
||||||
<f7-list-button v-if="isEnableApplicationLock" @click="disable(null)">{{ $t('Disable') }}</f7-list-button>
|
<f7-list-button v-if="isEnableApplicationLock" @click="disable(null)">{{ tt('Disable') }}</f7-list-button>
|
||||||
<f7-list-button v-if="!isEnableApplicationLock" @click="enable(null)">{{ $t('Enable') }}</f7-list-button>
|
<f7-list-button v-if="!isEnableApplicationLock" @click="enable(null)">{{ tt('Enable') }}</f7-list-button>
|
||||||
</f7-list>
|
</f7-list>
|
||||||
|
|
||||||
<pin-code-input-sheet :title="$t('PIN Code')"
|
<pin-code-input-sheet :title="tt('PIN Code')"
|
||||||
:hint="$t('Please enter a new 6-digit PIN code. The PIN code would encrypt your local data, so you need to enter it every time you open this app. If this PIN code is lost, you will need to log in again.')"
|
:hint="tt('Please enter a new 6-digit PIN code. The PIN code would encrypt your local data, so you need to enter it every time you open this app. If this PIN code is lost, you will need to log in again.')"
|
||||||
v-model:show="showInputPinCodeSheetForEnable"
|
v-model:show="showInputPinCodeSheetForEnable"
|
||||||
v-model="currentPinCodeForEnable"
|
v-model="currentPinCodeForEnable"
|
||||||
@pincode:confirm="enable">
|
@pincode:confirm="enable">
|
||||||
</pin-code-input-sheet>
|
</pin-code-input-sheet>
|
||||||
|
|
||||||
<pin-code-input-sheet :title="$t('PIN Code')"
|
<pin-code-input-sheet :title="tt('PIN Code')"
|
||||||
:hint="$t('Your current PIN code is required to disable application lock.')"
|
:hint="tt('Your current PIN code is required to disable application lock.')"
|
||||||
v-model:show="showInputPinCodeSheetForDisable"
|
v-model:show="showInputPinCodeSheetForDisable"
|
||||||
v-model="currentPinCodeForDisable"
|
v-model="currentPinCodeForDisable"
|
||||||
@pincode:confirm="disable">
|
@pincode:confirm="disable">
|
||||||
@@ -35,16 +35,18 @@
|
|||||||
</f7-page>
|
</f7-page>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import { mapStores } from 'pinia';
|
import { ref, watch, nextTick } from 'vue';
|
||||||
|
|
||||||
|
import { useI18n } from '@/locales/helpers.ts';
|
||||||
|
import { useI18nUIComponents, showLoading, hideLoading } from '@/lib/ui/mobile.ts';
|
||||||
|
import { useAppLockPageBase } from '@/views/base/settings/AppLockPageBase.ts';
|
||||||
|
|
||||||
import { useSettingsStore } from '@/stores/setting.ts';
|
import { useSettingsStore } from '@/stores/setting.ts';
|
||||||
import { useUserStore } from '@/stores/user.ts';
|
import { useUserStore } from '@/stores/user.ts';
|
||||||
import { useTransactionsStore } from '@/stores/transaction.js';
|
import { useTransactionsStore } from '@/stores/transaction.js';
|
||||||
|
|
||||||
import {
|
import { registerWebAuthnCredential } from '@/lib/webauthn.ts';
|
||||||
isWebAuthnCompletelySupported,
|
|
||||||
registerWebAuthnCredential
|
|
||||||
} from '@/lib/webauthn.ts';
|
|
||||||
import {
|
import {
|
||||||
getUserAppLockState,
|
getUserAppLockState,
|
||||||
encryptToken,
|
encryptToken,
|
||||||
@@ -55,140 +57,127 @@ import {
|
|||||||
} from '@/lib/userstate.ts';
|
} from '@/lib/userstate.ts';
|
||||||
import logger from '@/lib/logger.ts';
|
import logger from '@/lib/logger.ts';
|
||||||
|
|
||||||
export default {
|
const { tt } = useI18n();
|
||||||
data() {
|
const { showToast } = useI18nUIComponents();
|
||||||
return {
|
|
||||||
isSupportedWebAuthn: false,
|
|
||||||
currentPinCodeForEnable: '',
|
|
||||||
currentPinCodeForDisable: '',
|
|
||||||
showInputPinCodeSheetForEnable: false,
|
|
||||||
showInputPinCodeSheetForDisable: false
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapStores(useSettingsStore, useUserStore, useTransactionsStore),
|
|
||||||
isEnableApplicationLock: {
|
|
||||||
get: function () {
|
|
||||||
return this.settingsStore.appSettings.applicationLock;
|
|
||||||
},
|
|
||||||
set: function (value) {
|
|
||||||
this.settingsStore.setEnableApplicationLock(value);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
isEnableApplicationLockWebAuthn: {
|
|
||||||
get: function () {
|
|
||||||
return this.settingsStore.appSettings.applicationLockWebAuthn;
|
|
||||||
},
|
|
||||||
set: function (value) {
|
|
||||||
this.settingsStore.setEnableApplicationLockWebAuthn(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
isEnableApplicationLockWebAuthn: function (newValue) {
|
|
||||||
const self = this;
|
|
||||||
|
|
||||||
if (newValue) {
|
const { isSupportedWebAuthn, isEnableApplicationLock, isEnableApplicationLockWebAuthn } = useAppLockPageBase();
|
||||||
self.$showLoading();
|
|
||||||
|
|
||||||
registerWebAuthnCredential(
|
const settingsStore = useSettingsStore();
|
||||||
getUserAppLockState(),
|
const userStore = useUserStore();
|
||||||
self.userStore.currentUserBasicInfo,
|
const transactionsStore = useTransactionsStore();
|
||||||
).then(({ id }) => {
|
|
||||||
self.$hideLoading();
|
|
||||||
|
|
||||||
saveWebAuthnConfig(id);
|
const currentPinCodeForEnable = ref<string>('');
|
||||||
self.settingsStore.setEnableApplicationLockWebAuthn(true);
|
const currentPinCodeForDisable = ref<string>('');
|
||||||
self.$toast('You have enabled WebAuthn successfully');
|
const showInputPinCodeSheetForEnable = ref<boolean>(false);
|
||||||
}).catch(error => {
|
const showInputPinCodeSheetForDisable = ref<boolean>(false);
|
||||||
logger.error('failed to enable WebAuthn', error);
|
|
||||||
|
|
||||||
self.$hideLoading();
|
function enable(pinCode: string | null): void {
|
||||||
|
if (settingsStore.appSettings.applicationLock) {
|
||||||
if (error.notSupported) {
|
showToast('Application lock has been enabled');
|
||||||
self.$toast('WebAuth is not supported on this device');
|
return;
|
||||||
} else if (error.name === 'NotAllowedError') {
|
|
||||||
self.$toast('User has canceled authentication');
|
|
||||||
} else if (error.invalid) {
|
|
||||||
self.$toast('Failed to enable WebAuthn');
|
|
||||||
} else {
|
|
||||||
self.$toast('User has canceled or this device does not support WebAuthn');
|
|
||||||
}
|
|
||||||
|
|
||||||
self.isEnableApplicationLockWebAuthn = false;
|
|
||||||
self.settingsStore.setEnableApplicationLockWebAuthn(false);
|
|
||||||
clearWebAuthnConfig();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
self.settingsStore.setEnableApplicationLockWebAuthn(false);
|
|
||||||
clearWebAuthnConfig();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created() {
|
|
||||||
const self = this;
|
|
||||||
isWebAuthnCompletelySupported().then(result => {
|
|
||||||
self.isSupportedWebAuthn = result;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
enable(pinCode) {
|
|
||||||
if (this.settingsStore.appSettings.applicationLock) {
|
|
||||||
this.$alert('Application lock has been enabled');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!pinCode) {
|
|
||||||
this.showInputPinCodeSheetForEnable = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.currentPinCodeForEnable || this.currentPinCodeForEnable.length !== 6) {
|
|
||||||
this.$alert('Invalid PIN code');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = this.userStore.currentUserBasicInfo;
|
|
||||||
|
|
||||||
if (!user || !user.username) {
|
|
||||||
this.$alert('An error occurred');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
encryptToken(user.username, pinCode);
|
|
||||||
this.settingsStore.setEnableApplicationLock(true);
|
|
||||||
this.transactionsStore.saveTransactionDraft();
|
|
||||||
|
|
||||||
this.settingsStore.setEnableApplicationLockWebAuthn(false);
|
|
||||||
clearWebAuthnConfig();
|
|
||||||
|
|
||||||
this.showInputPinCodeSheetForEnable = false;
|
|
||||||
},
|
|
||||||
disable(pinCode) {
|
|
||||||
if (!this.settingsStore.appSettings.applicationLock) {
|
|
||||||
this.$alert('Application lock is not enabled');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!pinCode) {
|
|
||||||
this.showInputPinCodeSheetForDisable = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isCorrectPinCode(pinCode)) {
|
|
||||||
this.$alert('Incorrect PIN code');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
decryptToken();
|
|
||||||
this.settingsStore.setEnableApplicationLock(false);
|
|
||||||
this.transactionsStore.saveTransactionDraft();
|
|
||||||
|
|
||||||
this.settingsStore.setEnableApplicationLockWebAuthn(false);
|
|
||||||
clearWebAuthnConfig();
|
|
||||||
|
|
||||||
this.showInputPinCodeSheetForDisable = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!pinCode) {
|
||||||
|
currentPinCodeForEnable.value = '';
|
||||||
|
showInputPinCodeSheetForEnable.value = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentPinCodeForEnable.value || currentPinCodeForEnable.value.length !== 6) {
|
||||||
|
nextTick(() => {
|
||||||
|
currentPinCodeForEnable.value = '';
|
||||||
|
});
|
||||||
|
showToast('Invalid PIN code');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = userStore.currentUserBasicInfo;
|
||||||
|
|
||||||
|
if (!user || !user.username) {
|
||||||
|
nextTick(() => {
|
||||||
|
currentPinCodeForEnable.value = '';
|
||||||
|
});
|
||||||
|
showToast('An error occurred');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
encryptToken(user.username, pinCode);
|
||||||
|
settingsStore.setEnableApplicationLock(true);
|
||||||
|
transactionsStore.saveTransactionDraft();
|
||||||
|
|
||||||
|
settingsStore.setEnableApplicationLockWebAuthn(false);
|
||||||
|
clearWebAuthnConfig();
|
||||||
|
|
||||||
|
showInputPinCodeSheetForEnable.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function disable(pinCode: string | null): void {
|
||||||
|
if (!settingsStore.appSettings.applicationLock) {
|
||||||
|
showToast('Application lock is not enabled');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pinCode) {
|
||||||
|
currentPinCodeForDisable.value = '';
|
||||||
|
showInputPinCodeSheetForDisable.value = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isCorrectPinCode(pinCode)) {
|
||||||
|
nextTick(() => {
|
||||||
|
currentPinCodeForDisable.value = '';
|
||||||
|
});
|
||||||
|
showToast('Incorrect PIN code');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
decryptToken();
|
||||||
|
settingsStore.setEnableApplicationLock(false);
|
||||||
|
transactionsStore.saveTransactionDraft();
|
||||||
|
|
||||||
|
settingsStore.setEnableApplicationLockWebAuthn(false);
|
||||||
|
clearWebAuthnConfig();
|
||||||
|
|
||||||
|
showInputPinCodeSheetForDisable.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(isEnableApplicationLockWebAuthn, (newValue) => {
|
||||||
|
const userAppLockState = getUserAppLockState();
|
||||||
|
|
||||||
|
if (newValue && userAppLockState && userStore.currentUserBasicInfo) {
|
||||||
|
showLoading();
|
||||||
|
|
||||||
|
registerWebAuthnCredential(
|
||||||
|
userAppLockState,
|
||||||
|
userStore.currentUserBasicInfo,
|
||||||
|
).then(({ id }) => {
|
||||||
|
hideLoading();
|
||||||
|
|
||||||
|
saveWebAuthnConfig(id);
|
||||||
|
settingsStore.setEnableApplicationLockWebAuthn(true);
|
||||||
|
showToast('You have enabled WebAuthn successfully');
|
||||||
|
}).catch(error => {
|
||||||
|
logger.error('failed to enable WebAuthn', error);
|
||||||
|
|
||||||
|
hideLoading();
|
||||||
|
|
||||||
|
if (error.notSupported) {
|
||||||
|
showToast('WebAuth is not supported on this device');
|
||||||
|
} else if (error.name === 'NotAllowedError') {
|
||||||
|
showToast('User has canceled authentication');
|
||||||
|
} else if (error.invalid) {
|
||||||
|
showToast('Failed to enable WebAuthn');
|
||||||
|
} else {
|
||||||
|
showToast('User has canceled or this device does not support WebAuthn');
|
||||||
|
}
|
||||||
|
|
||||||
|
isEnableApplicationLockWebAuthn.value = false;
|
||||||
|
settingsStore.setEnableApplicationLockWebAuthn(false);
|
||||||
|
clearWebAuthnConfig();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
settingsStore.setEnableApplicationLockWebAuthn(false);
|
||||||
|
clearWebAuthnConfig();
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user