diff --git a/src/desktop-main.js b/src/desktop-main.js index cf2b15a4..4fc2e04a 100644 --- a/src/desktop-main.js +++ b/src/desktop-main.js @@ -71,7 +71,7 @@ import draggable from 'vuedraggable'; import router from '@/router/desktop.js'; import { getVersion, getBuildTime } from '@/lib/version.ts'; -import userstate from '@/lib/userstate.js'; +import userstate from '@/lib/userstate.ts'; import { getI18nOptions, translateIf, diff --git a/src/lib/services.js b/src/lib/services.js index 417634ac..67f14098 100644 --- a/src/lib/services.js +++ b/src/lib/services.js @@ -12,7 +12,7 @@ import { BAIDU_MAP_JAVASCRIPT_URL, AMAP_JAVASCRIPT_URL } from '@/consts/api.ts'; -import userState from './userstate.js'; +import userState from './userstate.ts'; import { isDefined, isBoolean diff --git a/src/lib/userstate.js b/src/lib/userstate.ts similarity index 65% rename from src/lib/userstate.js rename to src/lib/userstate.ts index 25cdf00a..e008ea2b 100644 --- a/src/lib/userstate.js +++ b/src/lib/userstate.ts @@ -1,41 +1,56 @@ import CryptoJS from 'crypto-js'; +import type { UserBasicInfo } from '@/models/user.ts'; + import { isString, isObject } from './common.ts'; import { isEnableApplicationLock } from './settings.ts'; import logger from './logger.ts'; -const appLockSecretBaseStringPrefix = 'EBK_LOCK_SECRET_'; +const appLockSecretBaseStringPrefix: string = 'EBK_LOCK_SECRET_'; -const tokenLocalStorageKey = 'ebk_user_token'; -const webauthnConfigLocalStorageKey = 'ebk_user_webauthn_config'; -const userInfoLocalStorageKey = 'ebk_user_info'; -const transactionDraftLocalStorageKey = 'ebk_user_draft_transaction'; +const tokenLocalStorageKey: string = 'ebk_user_token'; +const webauthnConfigLocalStorageKey: string = 'ebk_user_webauthn_config'; +const userInfoLocalStorageKey: string = 'ebk_user_info'; +const transactionDraftLocalStorageKey: string = 'ebk_user_draft_transaction'; -const tokenSessionStorageKey = 'ebk_user_session_token'; -const encryptedTokenSessionStorageKey = 'ebk_user_session_encrypted_token'; -const appLockStateSessionStorageKey = 'ebk_user_app_lock_state'; // { 'username': '', secret: '' } +const tokenSessionStorageKey: string = 'ebk_user_session_token'; +const encryptedTokenSessionStorageKey: string = 'ebk_user_session_encrypted_token'; +const appLockStateSessionStorageKey: string = 'ebk_user_app_lock_state'; // { 'username': '', secret: '' } -function getAppLockSecret(pinCode) { +export interface ApplicationLockState { + readonly username: string; + readonly secret: string; +} + +export interface WebAuthnConfig { + readonly credentialId: string; +} + +function getAppLockSecret(pinCode: string): string { const hashedPinCode = CryptoJS.SHA256(appLockSecretBaseStringPrefix + pinCode).toString(); return hashedPinCode.substring(0, 24); // put secret into user id of webauthn (user id total length must less 64 bytes) } -function getEncryptedToken(token, appLockState) { +function getEncryptedToken(token: string, appLockState: ApplicationLockState): string { const key = CryptoJS.SHA256(`${appLockSecretBaseStringPrefix}|${appLockState.username}|${appLockState.secret}`).toString(); return CryptoJS.AES.encrypt(token, key).toString(); } -function getDecryptedToken(encryptedToken, appLockState) { +function getDecryptedToken(encryptedToken: string, appLockState: ApplicationLockState): string { const key = CryptoJS.SHA256(`${appLockSecretBaseStringPrefix}|${appLockState.username}|${appLockState.secret}`).toString(); const bytes = CryptoJS.AES.decrypt(encryptedToken, key); return bytes.toString(CryptoJS.enc.Utf8); } -function getToken() { +function getToken(): string | null { if (isEnableApplicationLock()) { const usedEncryptedToken = sessionStorage.getItem(encryptedTokenSessionStorageKey); const currentEncryptedToken = localStorage.getItem(tokenLocalStorageKey); + if (!usedEncryptedToken || !currentEncryptedToken) { + return null; + } + if (usedEncryptedToken === currentEncryptedToken) { return sessionStorage.getItem(tokenSessionStorageKey); } @@ -55,12 +70,17 @@ function getToken() { } } -function getUserInfo() { +function getUserInfo(): UserBasicInfo | null { const data = localStorage.getItem(userInfoLocalStorageKey); - return JSON.parse(data); + + if (!data) { + return null; + } + + return JSON.parse(data) as UserBasicInfo; } -function getUserTransactionDraft() { +function getUserTransactionDraft(): unknown | null { let data = localStorage.getItem(transactionDraftLocalStorageKey); if (!data) { @@ -75,16 +95,27 @@ function getUserTransactionDraft() { return JSON.parse(data); } -function getUserAppLockState() { +function getUserAppLockState(): ApplicationLockState { const data = sessionStorage.getItem(appLockStateSessionStorageKey); - return JSON.parse(data); + + if (!data) { + throw new Error('No app lock state in session storage'); + } + + const appLockState = JSON.parse(data); + + if (!appLockState || !appLockState.username || !appLockState.secret) { + throw new Error('App lock state is invalid'); + } + + return appLockState as ApplicationLockState; } -function isUserLogined() { +function isUserLogined(): boolean { return !!localStorage.getItem(tokenLocalStorageKey); } -function isUserUnlocked() { +function isUserUnlocked(): boolean { if (!isUserLogined()) { return false; } @@ -96,35 +127,50 @@ function isUserUnlocked() { return !!sessionStorage.getItem(appLockStateSessionStorageKey) && !!sessionStorage.getItem(tokenSessionStorageKey); } -function getWebAuthnCredentialId() { +function getWebAuthnCredentialId(): string | undefined { const webauthnConfigData = localStorage.getItem(webauthnConfigLocalStorageKey); - const webauthnConfig = JSON.parse(webauthnConfigData); + + if (!webauthnConfigData) { + return undefined; + } + + const webauthnConfig = JSON.parse(webauthnConfigData) as WebAuthnConfig; return webauthnConfig.credentialId; } -function saveWebAuthnConfig(credentialId) { - const webAuthnConfig = { +function saveWebAuthnConfig(credentialId: string): void { + const webAuthnConfig: WebAuthnConfig = { credentialId: credentialId }; localStorage.setItem(webauthnConfigLocalStorageKey, JSON.stringify(webAuthnConfig)); } -function clearWebAuthnConfig() { +function clearWebAuthnConfig(): void { localStorage.removeItem(webauthnConfigLocalStorageKey); } -function unlockTokenByWebAuthn(credentialId, userName, userSecret) { +function unlockTokenByWebAuthn(credentialId: string, userName: string, userSecret: string): void { const webauthnConfigData = localStorage.getItem(webauthnConfigLocalStorageKey); - const webauthnConfig = JSON.parse(webauthnConfigData); + + if (!webauthnConfigData) { + throw new Error('WebAuthn credential is not set'); + } + + const webauthnConfig = JSON.parse(webauthnConfigData) as WebAuthnConfig; if (webauthnConfig.credentialId !== credentialId) { - return false; + throw new Error('WebAuthn credential is invalid'); } const encryptedToken = localStorage.getItem(tokenLocalStorageKey); - const appLockState = { + + if (!encryptedToken) { + throw new Error('No token in local storage'); + } + + const appLockState: ApplicationLockState = { username: userName, secret: userSecret }; @@ -135,9 +181,14 @@ function unlockTokenByWebAuthn(credentialId, userName, userSecret) { sessionStorage.setItem(tokenSessionStorageKey, token); } -function unlockTokenByPinCode(userName, pinCode) { +function unlockTokenByPinCode(userName: string, pinCode: string): void { const encryptedToken = localStorage.getItem(tokenLocalStorageKey); - const appLockState = { + + if (!encryptedToken) { + throw new Error('No token in local storage'); + } + + const appLockState: ApplicationLockState = { username: userName, secret: getAppLockSecret(pinCode) }; @@ -148,9 +199,14 @@ function unlockTokenByPinCode(userName, pinCode) { sessionStorage.setItem(tokenSessionStorageKey, token); } -function encryptToken(userName, pinCode) { +function encryptToken(userName: string, pinCode: string): void { const token = localStorage.getItem(tokenLocalStorageKey); - const appLockState = { + + if (!token) { + throw new Error('No token in local storage'); + } + + const appLockState: ApplicationLockState = { username: userName, secret: getAppLockSecret(pinCode) }; @@ -162,23 +218,27 @@ function encryptToken(userName, pinCode) { localStorage.setItem(tokenLocalStorageKey, encryptedToken); } -function decryptToken() { +function decryptToken(): void { const token = sessionStorage.getItem(tokenSessionStorageKey); + if (!token) { + throw new Error('No token in session storage'); + } + localStorage.setItem(tokenLocalStorageKey, token); sessionStorage.removeItem(tokenSessionStorageKey); sessionStorage.removeItem(encryptedTokenSessionStorageKey); sessionStorage.removeItem(appLockStateSessionStorageKey); } -function isCorrectPinCode(pinCode) { +function isCorrectPinCode(pinCode: string): boolean { const secret = getAppLockSecret(pinCode); const appLockState = getUserAppLockState(); return appLockState && secret === appLockState.secret; } -function updateToken(token) { +function updateToken(token: string): void { if (isString(token)) { if (isEnableApplicationLock()) { const appLockState = getUserAppLockState(); @@ -193,13 +253,13 @@ function updateToken(token) { } } -function updateUserInfo(user) { +function updateUserInfo(user: UserBasicInfo): void { if (isObject(user)) { localStorage.setItem(userInfoLocalStorageKey, JSON.stringify(user)); } } -function updateUserTransactionDraft(transaction) { +function updateUserTransactionDraft(transaction: unknown): void { if (!isObject(transaction)) { return; } @@ -214,21 +274,21 @@ function updateUserTransactionDraft(transaction) { localStorage.setItem(transactionDraftLocalStorageKey, data); } -function clearUserInfo() { +function clearUserInfo(): void { localStorage.removeItem(userInfoLocalStorageKey); } -function clearUserTransactionDraft() { +function clearUserTransactionDraft(): void { localStorage.removeItem(transactionDraftLocalStorageKey); } -function clearSessionToken() { +function clearSessionToken(): void { sessionStorage.removeItem(tokenSessionStorageKey); sessionStorage.removeItem(encryptedTokenSessionStorageKey); sessionStorage.removeItem(appLockStateSessionStorageKey); } -function clearTokenAndUserInfo(clearAppLockState) { +function clearTokenAndUserInfo(clearAppLockState: boolean): void { if (clearAppLockState) { sessionStorage.removeItem(appLockStateSessionStorageKey); } diff --git a/src/mobile-main.js b/src/mobile-main.js index 6b7b628c..50a9cd76 100644 --- a/src/mobile-main.js +++ b/src/mobile-main.js @@ -80,7 +80,7 @@ import VueDatePicker from '@vuepic/vue-datepicker'; import '@vuepic/vue-datepicker/dist/main.css'; import { getVersion, getBuildTime } from '@/lib/version.ts'; -import userstate from '@/lib/userstate.js'; +import userstate from '@/lib/userstate.ts'; import { getI18nOptions, translateIf, diff --git a/src/models/user.ts b/src/models/user.ts new file mode 100644 index 00000000..96cc67c0 --- /dev/null +++ b/src/models/user.ts @@ -0,0 +1,23 @@ +export interface UserBasicInfo { + readonly username: string; + readonly email: string; + readonly nickname: string; + readonly avatar: string; + readonly avatarProvider?: string; + readonly defaultAccountId: string; + readonly transactionEditScope: number; + readonly language: string; + readonly defaultCurrency: string; + readonly firstDayOfWeek: number; + readonly longDateFormat: number; + readonly shortDateFormat: number; + readonly longTimeFormat: number; + readonly shortTimeFormat: number; + readonly decimalSeparator: number; + readonly digitGroupingSymbol: number; + readonly digitGrouping: number; + readonly currencyDisplayType: number; + readonly expenseAmountColor: number; + readonly incomeAmountColor: number; + readonly emailVerified: boolean; +} diff --git a/src/router/desktop.js b/src/router/desktop.js index 0046ba37..5b6c0c91 100644 --- a/src/router/desktop.js +++ b/src/router/desktop.js @@ -1,7 +1,7 @@ import { createRouter, createWebHashHistory } from 'vue-router'; import { TemplateType } from '@/core/template.ts'; -import userState from '@/lib/userstate.js'; +import userState from '@/lib/userstate.ts'; import MainLayout from '@/views/desktop/MainLayout.vue'; import LoginPage from '@/views/desktop/LoginPage.vue'; diff --git a/src/router/mobile.js b/src/router/mobile.js index 9e84e6ba..e1e984dc 100644 --- a/src/router/mobile.js +++ b/src/router/mobile.js @@ -1,4 +1,4 @@ -import userState from '@/lib/userstate.js'; +import userState from '@/lib/userstate.ts'; import HomePage from '@/views/mobile/HomePage.vue'; import LoginPage from '@/views/mobile/LoginPage.vue'; diff --git a/src/stores/index.js b/src/stores/index.js index 64e230c8..e67daf5e 100644 --- a/src/stores/index.js +++ b/src/stores/index.js @@ -11,7 +11,7 @@ import { useOverviewStore } from './overview.js'; import { useStatisticsStore } from './statistics.js'; import { useExchangeRatesStore } from './exchangeRates.js'; -import userState from '@/lib/userstate.js'; +import userState from '@/lib/userstate.ts'; import services from '@/lib/services.js'; import logger from '@/lib/logger.ts'; import { isObject, isString } from '@/lib/common.ts'; diff --git a/src/stores/token.js b/src/stores/token.js index 1941545b..2fabddfb 100644 --- a/src/stores/token.js +++ b/src/stores/token.js @@ -2,7 +2,7 @@ import { defineStore } from 'pinia'; import { useUserStore } from './user.js'; -import userState from '@/lib/userstate.js'; +import userState from '@/lib/userstate.ts'; import services from '@/lib/services.js'; import logger from '@/lib/logger.ts'; import { isObject } from '@/lib/common.ts'; diff --git a/src/stores/transaction.js b/src/stores/transaction.js index 149cf65d..1dadf287 100644 --- a/src/stores/transaction.js +++ b/src/stores/transaction.js @@ -12,7 +12,7 @@ import { DateRange } from '@/core/datetime.ts'; import { CategoryType } from '@/core/category.ts'; import { TransactionType, TransactionTagFilterType } from '@/core/transaction.ts'; import { TRANSACTION_MIN_AMOUNT, TRANSACTION_MAX_AMOUNT } from '@/consts/transaction.ts'; -import userState from '@/lib/userstate.js'; +import userState from '@/lib/userstate.ts'; import services from '@/lib/services.js'; import logger from '@/lib/logger.ts'; import { diff --git a/src/stores/twoFactorAuth.js b/src/stores/twoFactorAuth.js index fc0a79d2..400c642f 100644 --- a/src/stores/twoFactorAuth.js +++ b/src/stores/twoFactorAuth.js @@ -1,6 +1,6 @@ import { defineStore } from 'pinia'; -import userState from '@/lib/userstate.js'; +import userState from '@/lib/userstate.ts'; import services from '@/lib/services.js'; import logger from '@/lib/logger.ts'; import { isBoolean } from '@/lib/common.ts'; diff --git a/src/stores/user.js b/src/stores/user.js index 33d946b5..f353f96e 100644 --- a/src/stores/user.js +++ b/src/stores/user.js @@ -2,7 +2,7 @@ import { defineStore } from 'pinia'; import { useSettingsStore } from './setting.js'; -import userState from '@/lib/userstate.js'; +import userState from '@/lib/userstate.ts'; import services from '@/lib/services.js'; import logger from '@/lib/logger.ts'; import {