diff --git a/src/lib/logger.js b/src/lib/logger.js new file mode 100644 index 00000000..34d50ae4 --- /dev/null +++ b/src/lib/logger.js @@ -0,0 +1,42 @@ +import settings from './settings.js'; + +function logDebug(msg, obj) { + if (settings.isEnableDebug()) { + if (obj) { + console.debug('[lab Debug] ' + msg, obj); + } else { + console.debug('[lab Debug] ' + msg); + } + } +} + +function logInfo(msg, obj) { + if (obj) { + console.info('[lab Info] ' + msg, obj); + } else { + console.info('[lab Info] ' + msg); + } +} + +function logWarn(msg, obj) { + if (obj) { + console.warn('[lab Warn] ' + msg, obj); + } else { + console.warn('[lab Warn] ' + msg); + } +} + +function logError(msg, obj) { + if (obj) { + console.error('[lab Error] ' + msg, obj); + } else { + console.error('[lab Error] ' + msg); + } +} + +export default { + debug: logDebug, + info: logInfo, + warn: logWarn, + error: logError +}; diff --git a/src/lib/settings.js b/src/lib/settings.js index 4f02f51f..8088aa36 100644 --- a/src/lib/settings.js +++ b/src/lib/settings.js @@ -5,6 +5,7 @@ const serverSettingsCookieKey = 'ACP_SETTINGS'; const defaultSettings = { lang: 'en', + debug: false, applicationLock: false, applicationLockWebAuthn: false, autoUpdateExchangeRatesData: true, @@ -75,6 +76,8 @@ function clearSettings() { export default { getLanguage: () => getOriginalOption('lang'), setLanguage: value => setOption('lang', value), + isEnableDebug: () => getOption('debug'), + setEnableDebug: value => setOption('debug', value), isEnableApplicationLock: () => getOption('applicationLock'), setEnableApplicationLock: value => setOption('applicationLock', value), isEnableApplicationLockWebAuthn: () => getOption('applicationLockWebAuthn'), diff --git a/src/lib/webauthn.js b/src/lib/webauthn.js index a299776b..e493e60b 100644 --- a/src/lib/webauthn.js +++ b/src/lib/webauthn.js @@ -1,4 +1,5 @@ import CBOR from 'cbor-js'; +import logger from './logger.js'; import utils from './utils.js'; const PUBLIC_KEY_CREDENTIAL_CREATION_OPTIONS_TEMPLATE = { @@ -72,6 +73,8 @@ function registerCredential({ username, nickname }, userSecret) { } }); + logger.debug('webauthn create options', publicKeyCredentialCreationOptions); + return navigator.credentials.create({ publicKey: publicKeyCredentialCreationOptions }).then(rawCredential => { @@ -80,15 +83,20 @@ function registerCredential({ username, nickname }, userSecret) { const challengeFromClientData = clientData && clientData.challenge ? atob(clientData.challenge) : null; + logger.debug('webauthn create raw response', rawCredential); + if (rawCredential && rawCredential.rawId && clientData && clientData.type === 'webauthn.create' && challengeFromClientData === challenge) { - - return { + const ret = { id: utils.base64encode(rawCredential.rawId), clientData: clientData, publicKey: publicKey, rawCredential: rawCredential }; + + logger.debug('webauthn create response', ret); + + return ret; } else { return Promise.reject({ invalid: true @@ -137,22 +145,30 @@ function verifyCredential(credentialId) { }); publicKeyCredentialRequestOptions.allowCredentials[0].id = Uint8Array.from(atob(credentialId), c=>c.charCodeAt(0)).buffer; + logger.debug('webauthn get options', publicKeyCredentialRequestOptions); + return navigator.credentials.get({ publicKey: publicKeyCredentialRequestOptions }).then(rawCredential => { const clientData = rawCredential ? parseClientData(rawCredential) : null; const challengeFromClientData = clientData && clientData.challenge ? atob(clientData.challenge) : null; + logger.debug('webauthn get raw response', rawCredential); + if (rawCredential && rawCredential.rawId && rawCredential.response && rawCredential.response.userHandle && clientData && clientData.type === 'webauthn.get' && challengeFromClientData === challenge) { - return { + const ret = { id: utils.base64encode(rawCredential.rawId), userSecret: utils.arrayBufferToString(rawCredential.response.userHandle), clientData: clientData, rawCredential: rawCredential }; + + logger.debug('webauthn get response', ret); + + return ret; } else { return Promise.reject({ invalid: true diff --git a/src/mobile-main.js b/src/mobile-main.js index afe09d13..3ebba542 100644 --- a/src/mobile-main.js +++ b/src/mobile-main.js @@ -20,6 +20,7 @@ import icons from './consts/icon.js'; import account from './consts/account.js'; import licenses from './consts/licenses.js'; import version from './lib/version.js'; +import logger from './lib/logger.js'; import settings from './lib/settings.js'; import services from './lib/services.js'; import userstate from './lib/userstate.js'; @@ -51,6 +52,7 @@ Vue.prototype.$constants = { account: account, }; Vue.prototype.$utilities = utils; +Vue.prototype.$logger = logger; Vue.prototype.$webauthn = webauthn; Vue.prototype.$settings = settings; Vue.prototype.$getDefaultLanguage = getDefaultLanguage; @@ -163,6 +165,12 @@ Vue.filter('accountIcon', (value) => accountIconFilter(value)); Vue.filter('tokenDevice', (value) => tokenDeviceFilter(value)); Vue.filter('tokenIcon', (value) => tokenIconFilter(value)); +if (settings.getLanguage()) { + logger.info(`Current language is ${settings.getLanguage()}`); +} else { + logger.info(`No language is set, use browser default ${getDefaultLanguage()}`); +} + Vue.prototype.$setLanguage(settings.getLanguage() || getDefaultLanguage()); if (userstate.isUserLogined()) { diff --git a/src/views/mobile/ApplicationLock.vue b/src/views/mobile/ApplicationLock.vue index 72b20b52..1a2ba31a 100644 --- a/src/views/mobile/ApplicationLock.vue +++ b/src/views/mobile/ApplicationLock.vue @@ -112,12 +112,14 @@ export default { self.$user.saveWebAuthnConfig(id); self.$settings.setEnableApplicationLockWebAuthn(true); self.$toast('You have enabled Face ID/Touch ID successfully'); - }).catch(({ notSupported, invalid }) => { + }).catch(error => { + self.$logger.error('failed to enable FaceID/Touch ID', error); + self.$hideLoading(); - if (notSupported) { + if (error.notSupported) { self.$toast('This device does not support Face ID/Touch ID'); - } else if (invalid) { + } else if (error.invalid) { self.$toast('Failed to enable Face ID/Touch ID'); } else { self.$toast('User has canceled or this device does not support Face ID/Touch ID'); diff --git a/src/views/mobile/ExchangeRates.vue b/src/views/mobile/ExchangeRates.vue index 077c5bcf..f8ec2091 100644 --- a/src/views/mobile/ExchangeRates.vue +++ b/src/views/mobile/ExchangeRates.vue @@ -137,6 +137,8 @@ export default { self.$toast('Exchange rates data has been updated'); }).catch(error => { + self.$logger.error('failed to get latest exchange rates data', error); + if (done) { done(); } diff --git a/src/views/mobile/Login.vue b/src/views/mobile/Login.vue index 71e893b3..d39b9be5 100644 --- a/src/views/mobile/Login.vue +++ b/src/views/mobile/Login.vue @@ -194,6 +194,8 @@ export default { router.refreshPage(); }).catch(error => { + self.$logger.error('failed to login', error); + self.logining = false; self.$hideLoading(); @@ -267,6 +269,8 @@ export default { self.show2faSheet = false; router.refreshPage(); }).catch(error => { + self.$logger.error('failed to verify 2fa', error); + self.verifying = false; self.$hideLoading(); diff --git a/src/views/mobile/Settings.vue b/src/views/mobile/Settings.vue index e51c970f..ea16592a 100644 --- a/src/views/mobile/Settings.vue +++ b/src/views/mobile/Settings.vue @@ -188,6 +188,8 @@ export default { self.$exchangeRates.clearExchangeRates(); router.navigate('/'); }).catch(error => { + self.$logger.error('failed to log out', error); + self.logouting = false; self.$hideLoading(); diff --git a/src/views/mobile/Signup.vue b/src/views/mobile/Signup.vue index 2ceb2ce1..63c1965c 100644 --- a/src/views/mobile/Signup.vue +++ b/src/views/mobile/Signup.vue @@ -166,6 +166,8 @@ export default { self.$toast('You have been successfully registered'); router.navigate('/'); }).catch(error => { + self.$logger.error('failed to sign up', error); + self.submitting = false; self.$hideLoading(); diff --git a/src/views/mobile/Unlock.vue b/src/views/mobile/Unlock.vue index d7ae762a..1b7119d5 100644 --- a/src/views/mobile/Unlock.vue +++ b/src/views/mobile/Unlock.vue @@ -43,10 +43,12 @@ export default { } router.refreshPage(); - }).catch(({ notSupported, invalid }) => { - if (notSupported) { + }).catch(error => { + self.$logger.error('failed to use webauthn to verify', error); + + if (error.notSupported) { self.$toast('This device does not support Face ID/Touch ID'); - } else if (invalid) { + } else if (error.invalid) { self.$toast('Failed to authenticate by Face ID/Touch ID'); } else { self.$toast('User has canceled or this device does not support Face ID/Touch ID'); @@ -72,6 +74,7 @@ export default { router.refreshPage(); } catch (ex) { + this.$logger.error('failed to unlock by pin code', ex); this.$alert('PIN code is wrong'); } }, diff --git a/src/views/mobile/accounts/AccountEdit.vue b/src/views/mobile/accounts/AccountEdit.vue index 916e9837..2c82dd92 100644 --- a/src/views/mobile/accounts/AccountEdit.vue +++ b/src/views/mobile/accounts/AccountEdit.vue @@ -394,6 +394,8 @@ export default { self.loading = false; }).catch(error => { + self.$logger.error('failed to load account info', error); + if (error.response && error.response.data && error.response.data.errorMessage) { self.$alert({ error: error.response.data }, () => { router.back(); @@ -548,6 +550,8 @@ export default { router.back('/account/list', { force: true }); }).catch(error => { + self.$logger.error('failed to save account', error); + self.submitting = false; self.$hideLoading(); diff --git a/src/views/mobile/accounts/AccountList.vue b/src/views/mobile/accounts/AccountList.vue index df6495b8..280f3a46 100644 --- a/src/views/mobile/accounts/AccountList.vue +++ b/src/views/mobile/accounts/AccountList.vue @@ -305,6 +305,8 @@ export default { self.accounts = self.$utilities.getCategorizedAccounts(data.result); self.loading = false; }).catch(error => { + self.$logger.error('failed to load account list', error); + if (error.response && error.response.data && error.response.data.errorMessage) { self.$alert({ error: error.response.data }, () => { router.back(); @@ -332,6 +334,8 @@ export default { self.accounts = self.$utilities.getCategorizedAccounts(data.result); }).catch(error => { + self.$logger.error('failed to reload account list', error); + done(); if (error.response && error.response.data && error.response.data.errorMessage) { @@ -477,6 +481,8 @@ export default { self.sortable = false; self.displayOrderModified = false; }).catch(error => { + self.$logger.error('failed to save accounts display order', error); + self.displayOrderSaving = false; self.$hideLoading(); @@ -514,6 +520,8 @@ export default { account.hidden = hidden; }).catch(error => { + self.$logger.error('failed to change account visibility', error); + self.$hideLoading(); if (error.response && error.response.data && error.response.data.errorMessage) { @@ -555,6 +563,8 @@ export default { } }); }).catch(error => { + self.$logger.error('failed to delete account', error); + self.$hideLoading(); if (error.response && error.response.data && error.response.data.errorMessage) { diff --git a/src/views/mobile/users/SessionList.vue b/src/views/mobile/users/SessionList.vue index d3c0b5a9..73680456 100644 --- a/src/views/mobile/users/SessionList.vue +++ b/src/views/mobile/users/SessionList.vue @@ -69,6 +69,8 @@ export default { self.tokens = data.result; self.loading = false; }).catch(error => { + self.$logger.error('failed to load token list', error); + if (error.response && error.response.data && error.response.data.errorMessage) { self.$alert({ error: error.response.data }, () => { router.back(); @@ -96,6 +98,8 @@ export default { self.tokens = data.result; }).catch(error => { + self.$logger.error('failed to reload token list', error); + done(); if (error.response && error.response.data && error.response.data.errorMessage) { @@ -132,6 +136,8 @@ export default { } }); }).catch(error => { + self.$logger.error('failed to revoke token', error); + self.$hideLoading(); if (error.response && error.response.data && error.response.data.errorMessage) { @@ -169,6 +175,8 @@ export default { self.$toast('You have logged out all other sessions'); }).catch(error => { + self.$logger.error('failed to revoke all tokens', error); + self.$hideLoading(); if (error.response && error.response.data && error.response.data.errorMessage) { diff --git a/src/views/mobile/users/TwoFactorAuth.vue b/src/views/mobile/users/TwoFactorAuth.vue index 21e020c6..0ab86b05 100644 --- a/src/views/mobile/users/TwoFactorAuth.vue +++ b/src/views/mobile/users/TwoFactorAuth.vue @@ -184,6 +184,8 @@ export default { self.status = data.result.enable; self.loading = false; }).catch(error => { + self.$logger.error('failed to get 2fa status', error); + if (error.response && error.response.data && error.response.data.errorMessage) { self.$alert({ error: error.response.data }, () => { router.back(); @@ -220,6 +222,8 @@ export default { self.showInputPasscodeSheetForEnable = true; }).catch(error => { + self.$logger.error('failed to request to enable 2fa', error); + self.enabling = false; self.$hideLoading(); @@ -262,6 +266,8 @@ export default { self.showBackupCodeSheet = true; } }).catch(error => { + self.$logger.error('failed to confirm to enable 2fa', error); + self.enableConfirming = false; self.$hideLoading(); @@ -300,6 +306,8 @@ export default { self.showInputPasswordSheetForDisable = false; self.$toast('Two factor authentication has been disabled'); }).catch(error => { + self.$logger.error('failed to disable 2fa', error); + self.disabling = false; self.$hideLoading(); @@ -339,6 +347,8 @@ export default { self.currentBackupCode = data.result.recoveryCodes.join('\n'); self.showBackupCodeSheet = true; }).catch(error => { + self.$logger.error('failed to regenerate 2fa recovery code', error); + self.regenerating = false; self.$hideLoading(); diff --git a/src/views/mobile/users/UserProfile.vue b/src/views/mobile/users/UserProfile.vue index 7c83f6ae..e84aa12e 100644 --- a/src/views/mobile/users/UserProfile.vue +++ b/src/views/mobile/users/UserProfile.vue @@ -189,6 +189,8 @@ export default { self.newProfile.defaultCurrency = self.oldProfile.defaultCurrency; self.loading = false; }).catch(error => { + self.$logger.error('failed to get user profile', error); + if (error.response && error.response.data && error.response.data.errorMessage) { self.$alert({ error: error.response.data }, () => { router.back(); @@ -251,6 +253,8 @@ export default { self.$toast('Your profile has been successfully updated'); router.back('/settings', { force: true }); }).catch(error => { + self.$logger.error('failed to save user profile', error); + self.saving = false; self.$hideLoading(); self.currentPassword = '';