From 75146a4aa6158870f3e6c61cf176ff31982aa874 Mon Sep 17 00:00:00 2001 From: Joao Leonardo Pereira Date: Sun, 22 Sep 2024 16:36:08 -0300 Subject: [PATCH] encryption madness --- .vscode/settings.json | 33 +- .../account-select-modal.component.html | 2 +- src/app/home/home.page.html | 29 +- src/app/home/home.page.ts | 304 ++++++++++++++---- src/app/models/app-version.enum.ts | 10 + src/app/models/encryption-options.model.ts | 4 +- .../accounts/local-account2fa.service.ts | 2 +- src/app/services/app-config.service.ts | 28 +- src/app/services/migration.service.ts | 65 +++- src/app/utils/version-utils.ts | 59 ++++ src/assets/i18n/en.json | 249 +++++++------- src/assets/i18n/pt.json | 244 +++++++------- src/environments/environment.ts | 2 +- src/global.scss | 4 + 14 files changed, 718 insertions(+), 317 deletions(-) create mode 100644 src/app/models/app-version.enum.ts create mode 100644 src/app/utils/version-utils.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 0abbfd2..dacb2df 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,34 @@ { - "typescript.preferences.autoImportFileExcludePatterns": ["@ionic/angular/common", "@ionic/angular/standalone"] + "typescript.preferences.autoImportFileExcludePatterns": [ + "@ionic/angular/common", + "@ionic/angular/standalone" + ], + "i18n-ally.localesPaths": [ + "src/assets/i18n", + ], + "i18n-ally.keystyle": "nested", + "i18n-ally.keysInUse": [ + "CRYPTO_API_NOT_SUPPORTED", + "ACCOUNT_SELECT_MODAL.DESELECT_ALL", + "ACCOUNT_SELECT_MODAL.SELECT_ALL", + "ADD_ACCOUNT_MODAL.ERROR_MSGS.INVALID_SESSION", + "ACCOUNT_SYNC.ERROR.CORRUPT_BACKUP_FILE", + "ACCOUNT_SYNC.ERROR.EMPTY_FILE", + "ACCOUNT_SYNC.ERROR.GENERIC_IMPORT_ERROR", + "ACCOUNT_SYNC.ERROR.NO_ACCOUNTS_SELECTED_TO_IMPORT", + "CONFIG_MENU.ENCRYPTION_ACTIVE", + "CONFIG_MENU.ENCRYPTION_INACTIVE", + "CRYPTO.ALREADY_DECRYPTED", + "CRYPTO.ALREADY_ENCRYPTED", + "CRYPTO.MISSING_ENCRYPTED_DATA", + "LOGIN.VALIDATION_MSGS.EMAIL_PATTERN", + "LOGIN.VALIDATION_MSGS.REQUIRED_EMAIL", + "LOGIN.VALIDATION_MSGS.REQUIRED_PASSWORD", + "LOGIN.ERROR_MSGS.DEFAULT_ERROR", + "LOGIN.ERROR_MSGS.INVALID_PASSWORD", + "LOGIN.ERROR_MSGS.TOO_MANY_ATTEMPTS_TRY_LATER", + "LOGIN.ERROR_MSGS.USER_NOT_FOUND", + "ACCOUNT_SERVICE.ERROR.ACCOUNT_NOT_FOUND", + "UTILS.ERROR_INVALID_SEMVER_FORMAT" + ] } diff --git a/src/app/components/account-select-modal/account-select-modal.component.html b/src/app/components/account-select-modal/account-select-modal.component.html index cab807f..d2bd3b5 100644 --- a/src/app/components/account-select-modal/account-select-modal.component.html +++ b/src/app/components/account-select-modal/account-select-modal.component.html @@ -9,7 +9,7 @@ {{ title || ("ACCOUNT_SELECT_MODAL.DEFAULT_TITLE" | translate) }} - {{ confirmText || ("CONFIRM_SELECTION.EXPORT" | translate) }} + {{ confirmText || ("ACCOUNT_SELECT_MODAL.CONFIRM_SELECTION" | translate) }} diff --git a/src/app/home/home.page.html b/src/app/home/home.page.html index a5a2f32..2d1e8b9 100644 --- a/src/app/home/home.page.html +++ b/src/app/home/home.page.html @@ -9,22 +9,10 @@ - - - {{ "CONFIG_MENU.UNLOCK" | translate }} - {{ "CONFIG_MENU.ADD_ACCOUNT" | translate }} - - - {{ "CONFIG_MENU.EXPORT_ACCOUNTS" | translate }} - - - - {{ "CONFIG_MENU.IMPORT_ACCOUNTS" | translate }} - {{ "CONFIG_MENU.ENCRYPTION_OPTIONS" | translate }} @@ -40,14 +28,18 @@ {{ "CONFIG_MENU.PERIODIC_CHECK" | translate }} - + + + {{ "CONFIG_MENU.EXPORT_ACCOUNTS" | translate }} + + + + {{ "CONFIG_MENU.IMPORT_ACCOUNTS" | translate }} + {{ "CONFIG_MENU.COLOR_MODE" | translate }} @@ -77,9 +69,12 @@ {{ "CONFIG_MENU.LOGOUT" | translate }} - + + + {{ versionInfo.versionName }} + diff --git a/src/app/home/home.page.ts b/src/app/home/home.page.ts index dd5ebbe..fc98e64 100644 --- a/src/app/home/home.page.ts +++ b/src/app/home/home.page.ts @@ -1,7 +1,7 @@ import { Component, HostListener, OnInit, ViewChild } from '@angular/core'; import { AuthenticationService } from '../services/authentication.service'; import { AlertController, IonModal, LoadingController, ModalController, NavController, ToastController } from '@ionic/angular'; -import { firstValueFrom, Observable } from 'rxjs'; +import { concatMap, firstValueFrom, map, Observable } from 'rxjs'; import { Account2FA } from '../models/account2FA.model'; import { Account2faService } from '../services/accounts/account2fa.service'; import { LogoService } from '../services/logo.service'; @@ -13,6 +13,7 @@ import { GlobalUtils } from '../utils/global-utils'; import { AccountSelectModalComponent } from '../components/account-select-modal/account-select-modal.component'; import { AppConfigService } from '../services/app-config.service'; import { ENCRYPTION_OPTIONS_PASSWORD_KEY, EncryptionOptions } from '../models/encryption-options.model'; +import { MigrationService } from '../services/migration.service'; @Component({ selector: 'app-home', @@ -67,6 +68,8 @@ export class HomePage implements OnInit { isWindowFocused: boolean = true isEncryptionActive: boolean = false shouldPeriodicCheckPassword: boolean = false + shouldAlertToActivateEncryption: boolean = true + versionInfo private systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)'); private isLandscape: boolean = false @@ -82,6 +85,7 @@ export class HomePage implements OnInit { private logoService: LogoService, private storageService: LocalStorageService, private configService: AppConfigService, + private migrationService: MigrationService, private translateService: TranslateService, private navCtrl: NavController, formBuilder: FormBuilder @@ -104,6 +108,8 @@ export class HomePage implements OnInit { Validators.pattern('^[1-9]+[0-9]*$') ])), }); + + this.versionInfo = this.configService.versionInfo } get accountListType() { @@ -142,6 +148,7 @@ export class HomePage implements OnInit { await this.setupEncryption(encryptionOptions) } await loading.present() + await this.migrationService.migrate() await this.loadAccounts() await loading.dismiss() } @@ -330,54 +337,55 @@ export class HomePage implements OnInit { async saveEncryptionOptions() { await this.configService.setEncryptionOptions({ encryptionActive: this.isEncryptionActive, - shouldPerformPeriodicCheck: this.shouldPeriodicCheckPassword + shouldPerformPeriodicCheck: this.shouldPeriodicCheckPassword, + shouldAlertToActivateEncryption: this.shouldAlertToActivateEncryption }) } - async lockAccountAction() { - const password = '1490' - const accountSelected = this.selectedAccount - console.log('account', { accountSelected }) + // async lockAccountAction() { + // const password = '1490' + // const accountSelected = this.selectedAccount + // console.log('account', { accountSelected }) - if (accountSelected) { - const account = Account2FA.fromDictionary(accountSelected.typeErased()) // copy account - try { - console.log('account before', { accountSelected }) - await account.lock(password) - this.lockedAccount = account - console.log('account locked', { account }) + // if (accountSelected) { + // const account = Account2FA.fromDictionary(accountSelected.typeErased()) // copy account + // try { + // console.log('account before', { accountSelected }) + // await account.lock(password) + // this.lockedAccount = account + // console.log('account locked', { account }) - } catch (error) { - console.error("Error locking account", error) - const message = await firstValueFrom(this.translateService.get('HOME.ERROR_LOCKING_ACCOUNT')) - const alert = await this.alertController.create({ - message, - buttons: ['OK'] - }) - await alert.present() - } - } - } - - async unlockAccountAction() { - const password = '1490' - const account = this.lockedAccount - console.log('account before', { account }) - if(account) { - try { - await account.unlock(password) - console.log('account unlocked', { account }) - } catch (error) { - console.error("Error unlocking account", error) - const message = await firstValueFrom(this.translateService.get('HOME.ERROR_UNLOCKING_ACCOUNT')) - const alert = await this.alertController.create({ - message, - buttons: ['OK'] - }) - await alert.present() - } - } - } + // } catch (error) { + // console.error("Error locking account", error) + + // const alert = await this.alertController.create({ + // message, + // buttons: ['OK'] + // }) + // await alert.present() + // } + // } + // } + + // async unlockAccountAction() { + // const password = '1490' + // const account = this.lockedAccount + // console.log('account before', { account }) + // if(account) { + // try { + // await account.unlock(password) + // console.log('account unlocked', { account }) + // } catch (error) { + // console.error("Error unlocking account", error) + + // const alert = await this.alertController.create({ + // message, + // buttons: ['OK'] + // }) + // await alert.present() + // } + // } + // } showPopover(e: Event) { this.popover.event = null @@ -435,7 +443,9 @@ export class HomePage implements OnInit { this.selectAccount(account) } catch (error: any) { await loading.dismiss() - const messageKey = error.message === 'INVALID_SESSION' ? 'ADD_ACCOUNT_MODAL.ERROR_INVALID_SESSION' : 'ADD_ACCOUNT_MODAL.ERROR_ADDING_ACCOUNT' + const messageKey = error.message === 'INVALID_SESSION' ? + await firstValueFrom(this.translateService.get('ADD_ACCOUNT_MODAL.ERROR_MSGS.INVALID_SESSION')) : + await firstValueFrom(this.translateService.get('ADD_ACCOUNT_MODAL.ERROR_MSGS.ERROR_ADDING_ACCOUNT')) const message = await firstValueFrom(this.translateService.get(messageKey)) const toast = await this.toastController.create({ message: message, @@ -620,51 +630,139 @@ export class HomePage implements OnInit { private async loadAccounts() { const accounts$ = await this.accountsService.getAccounts() - const lastSelectedAccountId: string | undefined = await this.storageService.get('lastSelectedAccountId') - if (lastSelectedAccountId) { - const accounts = await firstValueFrom(accounts$) - const lastSelectedAccount = accounts.find(account => account.id === lastSelectedAccountId) - if (lastSelectedAccount) { - this.selectAccount(lastSelectedAccount) - } - } this.accounts$ = accounts$ } + private async unlockAccounts(): Promise { + const encryptionOptions = await this.configService.getEncryptionOptions() + if(!encryptionOptions || !encryptionOptions.encryptionActive) { + return + } + + const password = await this.configService.getEncryptionKey() + if(!password) { + const errormessage = await firstValueFrom(this.translateService.get('HOME.ERRORS.UNABLE_TO_DECRYPT_PASSWORD_NOT_SET')) + throw new Error(errormessage) + } else { + this.accounts$ = this.accounts$.pipe(concatMap(async accounts => { + const unlockedAccounts = [] + for(const account of accounts) { + if(account.isLocked) { + try { + await account.unlock(password) + unlockedAccounts.push(account) + } catch (error) { + console.error("Error unlocking account", error) + } + } else { + unlockedAccounts.push(account) + } + } + return unlockedAccounts + })) + } + } + private async setupEncryption(encryptionOptions: EncryptionOptions) { // set page properties this.isEncryptionActive = encryptionOptions.encryptionActive this.shouldPeriodicCheckPassword = encryptionOptions.shouldPerformPeriodicCheck - + this.shouldAlertToActivateEncryption = encryptionOptions.shouldAlertToActivateEncryption if(this.isEncryptionActive) { - await this.setupEncryptionPassword() + const passwordSetupSuccess = await this.setupEncryptionPassword() + if(!passwordSetupSuccess) { + // Deactivate encryption + this.isEncryptionActive = false + + // show error message + const title = await firstValueFrom(this.translateService.get('HOME.ERRORS.PASSWORD_NOT_SET_TITLE')) + const message = await firstValueFrom(this.translateService.get('HOME.ERRORS.PASSWORD_NOT_SET')) + const alert = await this.alertController.create({ + header: title, + backdropDismiss: false, + message, + buttons: ['OK'] + }) + await alert.present() + await alert.onDidDismiss() + } + } else { + // alert to activate encryption + if (this.shouldAlertToActivateEncryption) { + await this.alertToActivateEncryption() + } } + + // periodicCheck } - private async setupEncryptionPassword() { - const password = await this.storageService.get(ENCRYPTION_OPTIONS_PASSWORD_KEY) + private async setupEncryptionPassword(): Promise { + const password = await this.configService.getEncryptionKey() + console.log({ password }) if(!password) { // show password prompt - const password = await this.promptPassword() - if(password) { - await this.storageService.set(ENCRYPTION_OPTIONS_PASSWORD_KEY, password) + const passwordData = await this.promptPassword() + if(passwordData && passwordData.password && passwordData.password === passwordData.passwordConfirmation) { + await this.configService.setEncryptionKey(passwordData.password) + return true + } else { + if(!passwordData || (!passwordData.password && !passwordData.passwordConfirmation)) { + return false + } + // show error message + const message = await firstValueFrom(this.translateService.get('HOME.ERRORS.PASSWORD_MISMATCH')) + const tryAgainLabel = await firstValueFrom(this.translateService.get('HOME.TRY_AGAIN')) + const cancelLabel = await firstValueFrom(this.translateService.get('HOME.CANCEL')) + const alert = await this.alertController.create({ + message, + backdropDismiss: false, + buttons: [ + { + text: cancelLabel, + role: 'cancel' + }, + { + text: tryAgainLabel, + role: 'try_again' + } + ] + }) + await alert.present() + + const { role } = await alert.onDidDismiss() + if(role === 'cancel') { + return false + } + // clear password and try again + await this.configService.clearEncryptionKey() + return await this.setupEncryptionPassword() } } + return true } - private async promptPassword(): Promise { + private async promptPassword(): Promise<{password: string, passwordConfirmation: string}> { const title = await firstValueFrom(this.translateService.get('HOME.PASSWORD_PROMPT_TITLE')) const message = await firstValueFrom(this.translateService.get('HOME.PASSWORD_PROMPT_MESSAGE')) + const passwordPlaceholder = await firstValueFrom(this.translateService.get('HOME.PASSWORD_PROMPT_PLACEHOLDER')) + const passwordConfirmationPlaceholder = await firstValueFrom(this.translateService.get('HOME.PASSWORD_PROMPT_CONFIRMATION_PLACEHOLDER')) const cancelText = await firstValueFrom(this.translateService.get('HOME.PASSWORD_PROMPT_CANCEL')) const okText = await firstValueFrom(this.translateService.get('HOME.PASSWORD_PROMPT_CONFIRM')) const alert = await this.alertController.create({ header: title, message, + backdropDismiss: false, inputs: [ { name: 'password', - type: 'password' + type: 'password', + placeholder: passwordPlaceholder + }, + { + name: 'passwordConfirmation', + type: 'password', + placeholder: passwordConfirmationPlaceholder } ], buttons: [ @@ -674,7 +772,7 @@ export class HomePage implements OnInit { }, { text: okText, handler: (data) => { - return data.password + return { password: data.password, passwordConfirmation: data.passwordConfirmation } } } ] @@ -682,6 +780,82 @@ export class HomePage implements OnInit { await alert.present() const { data } = await alert.onDidDismiss() - return data?.values?.password || '' + if(!data) { + return { password: '', passwordConfirmation: '' } + } + return data.values + } + + async alertToActivateEncryption() { + const title = await firstValueFrom(this.translateService.get('HOME.ENCRYPTION_ALERT.TITLE')) + const message = await firstValueFrom(this.translateService.get('HOME.ENCRYPTION_ALERT.MESSAGE')) + const enableLabel = await firstValueFrom(this.translateService.get('HOME.ENCRYPTION_ALERT.ENABLE_ENCRYPTION')) + const laterLabel = await firstValueFrom(this.translateService.get('HOME.ENCRYPTION_ALERT.LATER')) + const alert = await this.alertController.create({ + header: title, + message, + backdropDismiss: false, + inputs: [ + { + type: 'checkbox', + value: 'dontShowAgain', + label: await firstValueFrom(this.translateService.get('HOME.ENCRYPTION_ALERT.DONT_SHOW_AGAIN')) + } + ], + buttons: [ + { + text: laterLabel, + role: 'later', + handler: (data) => { + if(data && data[0] == 'dontShowAgain') { + return { dontShowAgain: true } + } + return { dontShowAgain: false } + } + }, + { + text: enableLabel, + role: 'enable' + } + ] + }) + await alert.present() + + const { data, role } = await alert.onDidDismiss() + console.log('alert result', { data, role }) + + if(data && data.dontShowAgain) { + this.shouldAlertToActivateEncryption = false + await this.saveEncryptionOptions() + } + + if(role === 'enable') { + const setupSuccess = await this.setupEncryptionPassword() + if(setupSuccess) { + this.isEncryptionActive = true + await this.saveEncryptionOptions() + } + } } -} + + async showVersionInfo(): Promise { + const versionInfo = this.configService.versionInfo + const title = await firstValueFrom(this.translateService.get('HOME.VERSION_INFO.VERSION_INFO_TITLE')) + const versionLabel = await firstValueFrom(this.translateService.get('HOME.VERSION_INFO.VERSION_LABEL')) + const buildDateLabel = await firstValueFrom(this.translateService.get('HOME.VERSION_INFO.VERSION_DATE')) + const gitHashLabel = await firstValueFrom(this.translateService.get('HOME.VERSION_INFO.GIT_HASH')) + const buttonLabel = await firstValueFrom(this.translateService.get('HOME.VERSION_INFO.OK_BUTTON')) + const message = ` +

${versionLabel}: ${versionInfo.versionName}

+

${buildDateLabel}: ${versionInfo.buildDate}

+

${gitHashLabel}: ${versionInfo.commitHash}

` + + const alert = await this.alertController.create({ + header: title, + message, + buttons: [buttonLabel] + }) + await alert.present() + } + +} \ No newline at end of file diff --git a/src/app/models/app-version.enum.ts b/src/app/models/app-version.enum.ts new file mode 100644 index 0000000..22c8f8a --- /dev/null +++ b/src/app/models/app-version.enum.ts @@ -0,0 +1,10 @@ +/** + * Enum representing different versions of the application. + * + * @enum {string} + */ +export enum AppVersion { + UNKNOWN = 'UNKNOWN', + V1_0_0 = '1.0.0', + V2_0_0 = '2.0.0', +} diff --git a/src/app/models/encryption-options.model.ts b/src/app/models/encryption-options.model.ts index 7831a5a..6cfc576 100644 --- a/src/app/models/encryption-options.model.ts +++ b/src/app/models/encryption-options.model.ts @@ -1,7 +1,9 @@ export interface EncryptionOptions { encryptionActive: boolean; shouldPerformPeriodicCheck: boolean; + shouldAlertToActivateEncryption: boolean; } export const ENCRYPTION_OPTIONS_KEY = 'encryptionOptions'; -export const ENCRYPTION_OPTIONS_PASSWORD_KEY = '_eok'; \ No newline at end of file +export const ENCRYPTION_OPTIONS_PASSWORD_KEY = '_eok'; +export const LAST_PASSWORD_CHECK_KEY = 'lastPasswordCheck'; \ No newline at end of file diff --git a/src/app/services/accounts/local-account2fa.service.ts b/src/app/services/accounts/local-account2fa.service.ts index 6147979..19c2812 100644 --- a/src/app/services/accounts/local-account2fa.service.ts +++ b/src/app/services/accounts/local-account2fa.service.ts @@ -48,7 +48,7 @@ export class LocalAccount2faService implements IAccount2FAProvider { const existing = this.accounts.find(a => a.id === account.id) if (!existing) { - throw new Error('ACCOUNT_NOT_FOUND') + throw new Error('ACCOUNT_SERVICE.ERROR.ACCOUNT_NOT_FOUND') } const index = this.accounts.indexOf(existing) diff --git a/src/app/services/app-config.service.ts b/src/app/services/app-config.service.ts index d3b7206..54670e2 100644 --- a/src/app/services/app-config.service.ts +++ b/src/app/services/app-config.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; import { LocalStorageService } from './local-storage.service'; import { environment } from 'src/environments/environment'; -import { ENCRYPTION_OPTIONS_KEY, EncryptionOptions } from '../models/encryption-options.model'; +import { ENCRYPTION_OPTIONS_KEY, ENCRYPTION_OPTIONS_PASSWORD_KEY, EncryptionOptions, LAST_PASSWORD_CHECK_KEY } from '../models/encryption-options.model'; @Injectable({ providedIn: 'root' @@ -10,10 +10,15 @@ export class AppConfigService { constructor(private localStorage: LocalStorageService) { } + get versionInfo() { + return environment.versionConfig; + } + async isOfflineMode() { const isOfflinePref = await this.localStorage.get('isOfflineMode') ?? false; // if isOfflinePref is not set, use the environment variable return isOfflinePref ?? environment.isOfflineEnv; + } async setOfflineMode(isOffline: boolean) { @@ -32,6 +37,27 @@ export class AppConfigService { await this.localStorage.set(ENCRYPTION_OPTIONS_KEY, options); } + async getEncryptionKey() { + return await this.localStorage.get(ENCRYPTION_OPTIONS_PASSWORD_KEY); + } + + async setEncryptionKey(key: string) { + await this.localStorage.set(ENCRYPTION_OPTIONS_PASSWORD_KEY, key); + } + + async clearEncryptionKey() { + await this.localStorage.remove(ENCRYPTION_OPTIONS_PASSWORD_KEY); + } + + async getLastPasswordCheck() { + return await this.localStorage.get(LAST_PASSWORD_CHECK_KEY); + } + + async setLastPasswordCheck(timestamp?: number) { + const lastCheck = timestamp ?? Date.now(); + await this.localStorage.set(LAST_PASSWORD_CHECK_KEY, lastCheck); + } + static supportsCryptoAPI(): boolean { return !!(crypto && crypto.subtle) } diff --git a/src/app/services/migration.service.ts b/src/app/services/migration.service.ts index 181ca36..5d03525 100644 --- a/src/app/services/migration.service.ts +++ b/src/app/services/migration.service.ts @@ -1,9 +1,70 @@ import { Injectable } from '@angular/core'; +import { environment } from 'src/environments/environment'; +import { LocalStorageService } from './local-storage.service'; +import { AppVersion } from '../models/app-version.enum'; +import { VersionUtils } from '../utils/version-utils'; +import { AppConfigService } from './app-config.service'; +import { EncryptionOptions } from '../models/encryption-options.model'; @Injectable({ providedIn: 'root' }) + export class MigrationService { - - constructor() { } + constructor(private localStorage: LocalStorageService, private appConfigService: AppConfigService) { } + + async migrate() { + const dataVersion = await this.localStorage.get('data_version') ?? '0.0.0' + console.log('Current version:', dataVersion) + + const appVersion = environment.versionConfig.versionNumber + console.log('App version:', appVersion ) + const migrationsToRun = Object.values(AppVersion) + .filter(version => version > dataVersion && version <= appVersion) + .map(versionString => VersionUtils.appVersionFromVersionString(versionString)) + .sort(VersionUtils.appVersionCompare) + + if (migrationsToRun.length === 0) { + console.log('No migrations to run.') + return + } + + console.log('Running migrations:', migrationsToRun) + for(const version of migrationsToRun) { + await this.runMigration(version) + } + } + + private async runMigration(version: AppVersion) { + switch(version) { + case AppVersion.V1_0_0: + await this.migrateToV1_0_0() + break + case AppVersion.V2_0_0: + await this.migrateToV2_0_0() + break + default: + console.error('Migration not implemented for version:', version) + } + } + + private async migrateToV1_0_0() { + console.log('Migrating to v1.0.0') + await this.localStorage.set('data_version', '1.0.0') + } + + private async migrateToV2_0_0() { + console.log('Migrating to v2.0.0') + + // Initial encryption options + const encryptionOptions: EncryptionOptions = { + encryptionActive: false, + shouldPerformPeriodicCheck: false, + shouldAlertToActivateEncryption: true + } + await this.appConfigService.setEncryptionOptions(encryptionOptions) + await this.appConfigService.setLastPasswordCheck(0) + + await this.localStorage.set('data_version', '2.0.0') + } } diff --git a/src/app/utils/version-utils.ts b/src/app/utils/version-utils.ts new file mode 100644 index 0000000..eafafcb --- /dev/null +++ b/src/app/utils/version-utils.ts @@ -0,0 +1,59 @@ +import { AppVersion } from "../models/app-version.enum" + +export class VersionUtils { + /** + * Converts a version string to the corresponding AppVersion enum value. + * + * @param {string} version - The version string to convert. + * @returns {AppVersion} The corresponding AppVersion enum value. + */ + static appVersionFromVersionString(version: string): AppVersion { + switch (version) { + case '1.0.0': + return AppVersion.V1_0_0 + case '2.0.0': + return AppVersion.V2_0_0 + default: + return AppVersion.UNKNOWN + } + } + + /** + * Compares two AppVersion enum values. + * + * @param {AppVersion} a - The first AppVersion to compare. + * @param {AppVersion} b - The second AppVersion to compare. + * @returns {number} A negative number if a < b, zero if a === b, and a positive number if a > b. + */ + static appVersionCompare(a: AppVersion, b: AppVersion): number { + return Object.values(AppVersion).indexOf(a) - Object.values(AppVersion).indexOf(b) + } + + /** + * Compares two version strings in semver format. + * + * @param {string} a - The first version string to compare. + * @param {string} b - The second version string to compare. + * @returns {number} A negative number if a < b, zero if a === b, and a positive number if a > b. + */ + static semverCompare(a: string, b: string): number { + const aParts = a.split('.').map((part) => parseInt(part)) + const bParts = b.split('.').map((part) => parseInt(part)) + for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) { + if(isNaN(aParts[i]) || isNaN(bParts[i])) { + throw new Error('UTILS.ERROR_INVALID_SEMVER_FORMAT') + } + + const aPart = aParts[i] ?? 0 + const bPart = bParts[i] ?? 0 + + if (aPart < bPart) { + return -1 + } else if (aPart > bPart) { + return 1 + } + } + + return 0 + } +} \ No newline at end of file diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 2d5e471..4f71df3 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -1,120 +1,143 @@ { - "ACCOUNT_DETAIL": { - "CODE_COPIED": "Code copied to clipboard", - "SECONDS_SHORT": "s", - "SELECT_ACCOUNT_HINT": "Select an account to start..." - }, - "ACCOUNT_SELECT_MODAL": { - "CANCEL": "Cancel", - "CONFIRM_SELECTION": "Confirm", - "DEFAULT_TITLE": "Select Accounts", - "DESELECT_ALL": "Deselect All", - "SELECTION_COUNTER": "{{selected}}/{{total}} selected", - "SELECT_ALL": "Select All" - }, - "ACCOUNT_SERVICE": { - "ERROR": { - "ACCOUNT_NOT_FOUND": "Account not found" - } - }, - "ACCOUNT_SYNC": { - "ERROR": { - "CORRUPT_BACKUP_FILE": "Corrupted backup file", - "EMPTY_FILE": "The file is empty", - "GENERIC_IMPORT_ERROR": "Unknown error while importing accounts from backup", - "IMPORT_ERROR_TITLE": "Error importing accounts", - "NO_ACCOUNTS_SELECTED_TO_EXPORT": "You must select at least one account to export", - "NO_ACCOUNTS_SELECTED_TO_IMPORT": "No accounts selected to import" - }, - "EXPORTING_ACCOUNTS": "Exporting backup...", - "EXPORT_ACCOUNTS_MODAL_ACTION": "Export", - "EXPORT_ACCOUNTS_MODAL_TITLE": "Export Accounts", - "IMPORTING_ACCOUNTS": "Importing backup...", - "IMPORT_ACCOUNTS_MODAL_ACTION": "Import", - "IMPORT_ACCOUNTS_MODAL_TITLE": "Import Accounts" - }, - "ADD_ACCOUNT_MODAL": { - "ADDING_ACCOUNT": "Adding account...", - "ERROR_MSGS": { - "ERROR_ADDING_ACCOUNT": "Error adding account", - "ERROR_CAMERA_HEADER": "Camera unavailable", - "ERROR_CAMERA_MESSAGE": "

Camera access is required to scan QR codes, but it seems your device either lacks a camera or camera permissions were denied.

Please check your device/permissions settings and try again.

", - "INVALID_QR_CODE": "Invalid QR Code", - "INVALID_SESSION": "Invalid session", - "UNKNOWN_ERROR": "Unknown error" - }, - "EXAMPLI_GRATIA_SHORT": "e.g.", - "LABEL": "Label", - "LOADING_CAMERA": "Loading camera...", - "LOGO": "Logo", - "MANUAL_INPUT": "Manually input", - "NO_LOGO": "No Logo", - "SAVE": "Save Account", - "SCAN_QR_CODE": "Scan QR Code", - "SECRET_KEY": "Secret Key", - "TITLE": "Add Account", - "TOKEN_INTERVAL": "Token Interval (s)", - "TOKEN_SIZE": "Token Size" + "ACCOUNT_DETAIL": { + "CODE_COPIED": "Code copied to clipboard", + "SECONDS_SHORT": "s", + "SELECT_ACCOUNT_HINT": "Select an account to start..." + }, + "ACCOUNT_SELECT_MODAL": { + "CANCEL": "Cancel", + "CONFIRM_SELECTION": "Confirm", + "DEFAULT_TITLE": "Select Accounts", + "DESELECT_ALL": "Deselect All", + "SELECTION_COUNTER": "{{selected}}/{{total}} selected", + "SELECT_ALL": "Select All" + }, + "ACCOUNT_SERVICE": { + "ERROR": { + "ACCOUNT_NOT_FOUND": "Account not found" + } + }, + "ACCOUNT_SYNC": { + "ERROR": { + "CORRUPT_BACKUP_FILE": "Corrupted backup file", + "EMPTY_FILE": "The file is empty", + "GENERIC_IMPORT_ERROR": "Unknown error while importing accounts from backup", + "IMPORT_ERROR_TITLE": "Error importing accounts", + "NO_ACCOUNTS_SELECTED_TO_EXPORT": "You must select at least one account to export", + "NO_ACCOUNTS_SELECTED_TO_IMPORT": "No accounts selected to import" }, - "CONFIG_MENU": { - "ADD_ACCOUNT": "Add Account", - "COLOR_MODE": "Color Mode", - "COLOR_MODE_DARK": "Dark", - "COLOR_MODE_LIGHT": "Light", - "COLOR_MODE_SYSTEM": "System", - "ENCRYPTION_ACTIVE": "Active", - "ENCRYPTION_INACTIVE": "Inactive", - "ENCRYPTION_OPTIONS": "Encryption", - "ENCRYPT_ACCOUNTS": "Encrypt Accounts", - "EXPORT_ACCOUNTS": "Export Backup", - "IMPORT_ACCOUNTS": "Import Backup", - "LOGIN": "Sign in", - "LOGOUT": "Sign out", - "PERIODIC_CHECK": "Periodic Password Check" + "EXPORTING_ACCOUNTS": "Exporting backup...", + "EXPORT_ACCOUNTS_MODAL_ACTION": "Export", + "EXPORT_ACCOUNTS_MODAL_TITLE": "Export Accounts", + "IMPORTING_ACCOUNTS": "Importing backup...", + "IMPORT_ACCOUNTS_MODAL_ACTION": "Import", + "IMPORT_ACCOUNTS_MODAL_TITLE": "Import Accounts" + }, + "ADD_ACCOUNT_MODAL": { + "ADDING_ACCOUNT": "Adding account...", + "ERROR_MSGS": { + "ERROR_ADDING_ACCOUNT": "Error adding account", + "ERROR_CAMERA_HEADER": "Camera unavailable", + "ERROR_CAMERA_MESSAGE": "

Camera access is required to scan QR codes, but it seems your device either lacks a camera or camera permissions were denied.

Please check your device/permissions settings and try again.

", + "INVALID_QR_CODE": "Invalid QR Code", + "INVALID_SESSION": "Invalid session" }, - "CRYPTO": { - "ALREADY_DECRYPTED": "Data is already decrypted", - "ALREADY_ENCRYPTED": "Data is already encrypted", - "CRYPTO_API_NOT_SUPPORTED": "Crypto API not supported by the browser", - "MISSING_ENCRYPTED_DATA": "Missing encrypted data" + "EXAMPLI_GRATIA_SHORT": "e.g.", + "LABEL": "Label", + "LOADING_CAMERA": "Loading camera...", + "LOGO": "Logo", + "MANUAL_INPUT": "Manually input", + "NO_LOGO": "No Logo", + "SAVE": "Save Account", + "SCAN_QR_CODE": "Scan QR Code", + "SECRET_KEY": "Secret Key", + "TITLE": "Add Account", + "TOKEN_INTERVAL": "Token Interval (s)", + "TOKEN_SIZE": "Token Size" + }, + "CONFIG_MENU": { + "ADD_ACCOUNT": "Add Account", + "COLOR_MODE": "Color Mode", + "COLOR_MODE_DARK": "Dark", + "COLOR_MODE_LIGHT": "Light", + "COLOR_MODE_SYSTEM": "System", + "ENCRYPTION_ACTIVE": "Active", + "ENCRYPTION_INACTIVE": "Inactive", + "ENCRYPTION_OPTIONS": "Encryption", + "ENCRYPT_ACCOUNTS": "Encrypt Accounts", + "EXPORT_ACCOUNTS": "Export Backup", + "IMPORT_ACCOUNTS": "Import Backup", + "LOGOUT": "Sign out", + "PERIODIC_CHECK": "Periodic Password Check" + }, + "CRYPTO": { + "ALREADY_DECRYPTED": "Data is already decrypted", + "ALREADY_ENCRYPTED": "Data is already encrypted", + "MISSING_ENCRYPTED_DATA": "Missing encrypted data" + }, + "CRYPTO_API_NOT_SUPPORTED": "Crypto API not supported by the browser", + "HOME": { + "CANCEL": "Cancel", + "CONFIRM_LOGOUT_MESSAGE": "

All local or unsynchronized accounts will be deleted!

This action cannot be undone.

", + "CONFIRM_LOGOUT_TITLE": "Are you sure you want to sign out?", + "CONFIRM_LOGOUT_YES": "CLEAR LOCAL DATA AND SIGN OUT", + "LOADING_ACCOUNTS": "Loading accounts...", + "LOADING_ACCOUNTS_FILE": "Loading accounts from file...", + "LOGGING_OUT": "Signing out...", + "PASSWORD_PROMPT_CANCEL": "Cancel", + "PASSWORD_PROMPT_CONFIRM": "Confirm", + "PASSWORD_PROMPT_MESSAGE": "In order to use the encryption feature, please enter a password", + "PASSWORD_PROMPT_PLACEHOLDER": "Password", + "PASSWORD_PROMPT_TITLE": "Enter the encryption password", + "VERSION_INFO": { + "GIT_HASH": "Commit hash", + "OK_BUTTON": "OK", + "VERSION_DATE": "Build date", + "VERSION_INFO_TITLE": "Version Info", + "VERSION_LABEL": "Version" }, - "CRYPTO_API_NOT_SUPPORTED": "Crypto API not supported by the browser", - "HOME": { - "CANCEL": "Cancel", - "CONFIRM_LOGOUT_MESSAGE": "

All local or unsynchronized accounts will be deleted!

This action cannot be undone.

", - "CONFIRM_LOGOUT_TITLE": "Are you sure you want to sign out?", - "CONFIRM_LOGOUT_YES": "CLEAR LOCAL DATA AND SIGN OUT", - "LOADING_ACCOUNTS": "Loading accounts...", - "LOADING_ACCOUNTS_FILE": "Loading accounts from file...", - "LOGGING_OUT": "Signing out...", - "PASSWORD_PROMPT_CANCEL": "Cancel", - "PASSWORD_PROMPT_CONFIRM": "Confirm", - "PASSWORD_PROMPT_MESSAGE": "In order to decrypt the accounts, please enter the encryption password", - "PASSWORD_PROMPT_TITLE": "Enter the encryption password" + "PASSWORD_PROMPT_CONFIRMATION_PLACEHOLDER": "Confirm Password", + "ERRORS": { + "PASSWORD_MISMATCH": "The passwords don't match", + "PASSWORD_NOT_SET": "A password was not set.

Encryption feature is inactive.

", + "PASSWORD_NOT_SET_TITLE": "Warning", + "UNABLE_TO_DECRYPT_PASSWORD_NOT_SET": "Unable to decrypt accounts as a password is not set" }, - "LOGIN": { - "AUTHENTICATING": "Authenticating...", - "CLOUD_SYNC": "Cloud sync", - "CONTINUE_OFFLINE": "Continue offline", - "EMAIL": "E-mail", - "ERROR_MSGS": { - "DEFAULT_ERROR": "Could not sign in, check the credentials and try again", - "INVALID_PASSWORD": "Invalid password", - "TOO_MANY_ATTEMPTS_TRY_LATER": "Too many attempts. Try again after 15 minutes", - "USER_NOT_FOUND": "User not found" - }, - "PASSWORD": "Password", - "SIGN_IN_BUTTON": "Sign in", - "VALIDATION_MSGS": { - "EMAIL_PATTERN": "Invalid e-mail", - "REQUIRED_EMAIL": "E-mail is required", - "REQUIRED_PASSWORD": "Password is required" - } + "TRY_AGAIN": "Try again", + "ENCRYPTION_ALERT": { + "TITLE": "Security alert", + "MESSAGE": "Enhance the security of your accounts by using the encryption feature.

Do you want to configure a password?

", + "ENABLE_ENCRYPTION": "Enable encryption", + "LATER": "Remind me later", + "DONT_SHOW_AGAIN": "Do not show this again" + } + }, + "LOGIN": { + "AUTHENTICATING": "Authenticating...", + "CLOUD_SYNC": "Cloud sync", + "CONTINUE_OFFLINE": "Continue offline", + "EMAIL": "E-mail", + "ERROR_MSGS": { + "DEFAULT_ERROR": "Could not sign in, check the credentials and try again", + "INVALID_PASSWORD": "Invalid password", + "TOO_MANY_ATTEMPTS_TRY_LATER": "Too many attempts. Try again after 15 minutes", + "USER_NOT_FOUND": "User not found" }, - "SEARCH": "Search", - "UPDATER": { - "UPDATE_AVAILABLE_BODY": "A new version of the app is available and will be installed automatically to get the latest features and bug fixes.", - "UPDATE_AVAILABLE_HEADER": "Update available", - "UPDATE_NOW": "Update now" + "PASSWORD": "Password", + "SIGN_IN_BUTTON": "Sign in", + "VALIDATION_MSGS": { + "EMAIL_PATTERN": "Invalid e-mail", + "REQUIRED_EMAIL": "E-mail is required", + "REQUIRED_PASSWORD": "Password is required" } -} \ No newline at end of file + }, + "SEARCH": "Search", + "UPDATER": { + "UPDATE_AVAILABLE_BODY": "A new version of the app is available and will be installed automatically to get the latest features and bug fixes.", + "UPDATE_AVAILABLE_HEADER": "Update available", + "UPDATE_NOW": "Update now" + }, + "UTILS": { + "ERROR_INVALID_SEMVER_FORMAT": "Invalid SemVer format" + } +} diff --git a/src/assets/i18n/pt.json b/src/assets/i18n/pt.json index a0d7989..41511ae 100644 --- a/src/assets/i18n/pt.json +++ b/src/assets/i18n/pt.json @@ -1,120 +1,136 @@ { - "ACCOUNT_DETAIL": { - "CODE_COPIED": "Código copiado para a área de transferência", - "SECONDS_SHORT": "s", - "SELECT_ACCOUNT_HINT": "Selecione uma conta para iniciar..." - }, - "ACCOUNT_SELECT_MODAL": { - "CANCEL": "Cancelar", - "CONFIRM_SELECTION": "Confirmar", - "DEFAULT_TITLE": "Selecionar Contas", - "DESELECT_ALL": "Limpar Seleção", - "SELECTION_COUNTER": "{{selected}} de {{total}} selecionada(s)", - "SELECT_ALL": "Selecionar Tudo" - }, - "ACCOUNT_SERVICE": { - "ERROR": { - "ACCOUNT_NOT_FOUND": "Conta não encontrada" - } - }, - "ACCOUNT_SYNC": { - "ERROR": { - "CORRUPT_BACKUP_FILE": "Arquivo de backup corrompido", - "EMPTY_FILE": "O arquivo está vazio", - "GENERIC_IMPORT_ERROR": "Erro desconhecido ao importar contas do backup", - "IMPORT_ERROR_TITLE": "Erro ao importar contas", - "NO_ACCOUNTS_SELECTED_TO_EXPORT": "Você deve selecionar pelo menos uma conta para exportar", - "NO_ACCOUNTS_SELECTED_TO_IMPORT": "Nenhuma conta selecionada para importar" - }, - "EXPORTING_ACCOUNTS": "Exportando backup...", - "EXPORT_ACCOUNTS_MODAL_ACTION": "Exportar", - "EXPORT_ACCOUNTS_MODAL_TITLE": "Exportar Contas", - "IMPORTING_ACCOUNTS": "Importando backup...", - "IMPORT_ACCOUNTS_MODAL_ACTION": "Importar", - "IMPORT_ACCOUNTS_MODAL_TITLE": "Importar Contas" - }, - "ADD_ACCOUNT_MODAL": { - "ADDING_ACCOUNT": "Adicionando conta...", - "ERROR_MSGS": { - "ERROR_ADDING_ACCOUNT": "Erro ao adicionar conta", - "ERROR_CAMERA_HEADER": "Câmera não disponível", - "ERROR_CAMERA_MESSAGE": "

O acesso à câmera é necessário para escanear QR codes, mas parece que seu dispositivo não possui uma câmera ou as permissões da câmera foram negadas.

Verifique as permissões e/ou configurações do seu dispositivo e tente novamente.

", - "INVALID_QR_CODE": "QR Code inválido", - "INVALID_SESSION": "Sessão inválida", - "UNKNOWN_ERROR": "Erro desconhecido" - }, - "EXAMPLI_GRATIA_SHORT": "ex.:", - "LABEL": "Rótulo", - "LOADING_CAMERA": "Carregando câmera...", - "LOGO": "Logo", - "MANUAL_INPUT": "Inserir manualmente", - "NO_LOGO": "Sem Logo", - "SAVE": "Salvar Conta", - "SCAN_QR_CODE": "Escanear QR Code", - "SECRET_KEY": "Chave-Segredo", - "TITLE": "Adicionar Conta", - "TOKEN_INTERVAL": "Intervalo do Token (s)", - "TOKEN_SIZE": "Tamanho do Token" - }, - "CONFIG_MENU": { - "ADD_ACCOUNT": "Adicionar Conta", - "COLOR_MODE": "Modo de Cor", - "COLOR_MODE_DARK": "Escuro", - "COLOR_MODE_LIGHT": "Claro", - "COLOR_MODE_SYSTEM": "Sistema", - "ENCRYPTION_ACTIVE": "Ativo", - "ENCRYPTION_INACTIVE": "Inativo", - "ENCRYPTION_OPTIONS": "Criptografia", - "ENCRYPT_ACCOUNTS": "Criptografar Contas", - "EXPORT_ACCOUNTS": "Exportar Backup", - "IMPORT_ACCOUNTS": "Importar Backup", - "LOGIN": "Entrar", - "LOGOUT": "Sair", - "PERIODIC_CHECK": "Verificação Periódica de Senha" + "ACCOUNT_DETAIL": { + "CODE_COPIED": "Código copiado para a área de transferência", + "SECONDS_SHORT": "s", + "SELECT_ACCOUNT_HINT": "Selecione uma conta para iniciar..." + }, + "ACCOUNT_SELECT_MODAL": { + "CANCEL": "Cancelar", + "CONFIRM_SELECTION": "Confirmar", + "DEFAULT_TITLE": "Selecionar Contas", + "DESELECT_ALL": "Limpar Seleção", + "SELECTION_COUNTER": "{{selected}} de {{total}} selecionada(s)", + "SELECT_ALL": "Selecionar Tudo" + }, + "ACCOUNT_SERVICE": { + "ERROR": { + "ACCOUNT_NOT_FOUND": "Conta não encontrada" + } + }, + "ACCOUNT_SYNC": { + "ERROR": { + "CORRUPT_BACKUP_FILE": "Arquivo de backup corrompido", + "EMPTY_FILE": "O arquivo está vazio", + "GENERIC_IMPORT_ERROR": "Erro desconhecido ao importar contas do backup", + "IMPORT_ERROR_TITLE": "Erro ao importar contas", + "NO_ACCOUNTS_SELECTED_TO_EXPORT": "Você deve selecionar pelo menos uma conta para exportar", + "NO_ACCOUNTS_SELECTED_TO_IMPORT": "Nenhuma conta selecionada para importar" }, - "CRYPTO": { - "ALREADY_DECRYPTED": "Os dados já estão descriptografados", - "ALREADY_ENCRYPTED": "Os dados já estão criptografados", - "CRYPTO_API_NOT_SUPPORTED": "API de criptografia não suportada pelo navegador", - "MISSING_ENCRYPTED_DATA": "Dados criptografados ausentes" + "EXPORTING_ACCOUNTS": "Exportando backup...", + "EXPORT_ACCOUNTS_MODAL_ACTION": "Exportar", + "EXPORT_ACCOUNTS_MODAL_TITLE": "Exportar Contas", + "IMPORTING_ACCOUNTS": "Importando backup...", + "IMPORT_ACCOUNTS_MODAL_ACTION": "Importar", + "IMPORT_ACCOUNTS_MODAL_TITLE": "Importar Contas" + }, + "ADD_ACCOUNT_MODAL": { + "ADDING_ACCOUNT": "Adicionando conta...", + "ERROR_MSGS": { + "ERROR_ADDING_ACCOUNT": "Erro ao adicionar conta", + "ERROR_CAMERA_HEADER": "Câmera não disponível", + "ERROR_CAMERA_MESSAGE": "

O acesso à câmera é necessário para escanear QR codes, mas parece que seu dispositivo não possui uma câmera ou as permissões da câmera foram negadas.

Verifique as permissões e/ou configurações do seu dispositivo e tente novamente.

", + "INVALID_QR_CODE": "QR Code inválido", + "INVALID_SESSION": "Sessão inválida" }, - "CRYPTO_API_NOT_SUPPORTED": "API de criptografia não suportada pelo navegador", - "HOME": { - "CANCEL": "Cancelar", - "CONFIRM_LOGOUT_MESSAGE": "

Todas as contas locais e não sincronizadas serão excluídas!

Esta ação não pode ser desfeita.

", - "CONFIRM_LOGOUT_TITLE": "Tem certeza de que deseja sair?", - "CONFIRM_LOGOUT_YES": "LIMPAR DADOS LOCAIS E SAIR", - "LOADING_ACCOUNTS": "Carregando contas...", - "LOADING_ACCOUNTS_FILE": "Carregando arquivo de contas...", - "LOGGING_OUT": "Saindo...", - "PASSWORD_PROMPT_CANCEL": "Cancelar", - "PASSWORD_PROMPT_CONFIRM": "Confirmar", - "PASSWORD_PROMPT_MESSAGE": "Para descriptografar as contas, insira a senha de criptografia", - "PASSWORD_PROMPT_TITLE": "Digite a senha de criptografia" + "EXAMPLI_GRATIA_SHORT": "ex.:", + "LABEL": "Rótulo", + "LOADING_CAMERA": "Carregando câmera...", + "LOGO": "Logo", + "MANUAL_INPUT": "Inserir manualmente", + "NO_LOGO": "Sem Logo", + "SAVE": "Salvar Conta", + "SCAN_QR_CODE": "Escanear QR Code", + "SECRET_KEY": "Chave-Segredo", + "TITLE": "Adicionar Conta", + "TOKEN_INTERVAL": "Intervalo do Token (s)", + "TOKEN_SIZE": "Tamanho do Token" + }, + "CONFIG_MENU": { + "ADD_ACCOUNT": "Adicionar Conta", + "COLOR_MODE": "Modo de Cor", + "COLOR_MODE_DARK": "Escuro", + "COLOR_MODE_LIGHT": "Claro", + "COLOR_MODE_SYSTEM": "Sistema", + "ENCRYPTION_ACTIVE": "Ativo", + "ENCRYPTION_INACTIVE": "Inativo", + "ENCRYPTION_OPTIONS": "Criptografia", + "ENCRYPT_ACCOUNTS": "Criptografar Contas", + "EXPORT_ACCOUNTS": "Exportar Backup", + "IMPORT_ACCOUNTS": "Importar Backup", + "LOGOUT": "Sair", + "PERIODIC_CHECK": "Re-checagem periódica" + }, + "CRYPTO": { + "ALREADY_DECRYPTED": "Os dados já estão descriptografados", + "ALREADY_ENCRYPTED": "Os dados já estão criptografados", + "MISSING_ENCRYPTED_DATA": "Dados criptografados ausentes" + }, + "CRYPTO_API_NOT_SUPPORTED": "API de criptografia não suportada pelo navegador", + "HOME": { + "CANCEL": "Cancelar", + "CONFIRM_LOGOUT_MESSAGE": "

Todas as contas locais e não sincronizadas serão excluídas!

Esta ação não pode ser desfeita.

", + "CONFIRM_LOGOUT_TITLE": "Tem certeza de que deseja sair?", + "CONFIRM_LOGOUT_YES": "LIMPAR DADOS LOCAIS E SAIR", + "LOADING_ACCOUNTS": "Carregando contas...", + "LOADING_ACCOUNTS_FILE": "Carregando arquivo de contas...", + "LOGGING_OUT": "Saindo...", + "PASSWORD_PROMPT_CANCEL": "Cancelar", + "PASSWORD_PROMPT_CONFIRM": "Confirmar", + "PASSWORD_PROMPT_MESSAGE": "Para usar a funcionalidade de criptografia, configure uma senha", + "PASSWORD_PROMPT_PLACEHOLDER": "Senha", + "PASSWORD_PROMPT_TITLE": "Digite a senha de criptografia", + "PASSWORD_PROMPT_CONFIRMATION_PLACEHOLDER": "Confirmar senha", + "ERRORS": { + "PASSWORD_MISMATCH": "A senha e a confirmação não conferem", + "PASSWORD_NOT_SET": "A senha não foi definida.

A funcionalidade de criptografia não foi ativada.

", + "PASSWORD_NOT_SET_TITLE": "Atenção", + "UNABLE_TO_DECRYPT_PASSWORD_NOT_SET": "Não é possível decodificar as contas sem uma senha configurada" }, - "LOGIN": { - "AUTHENTICATING": "Autenticando...", - "CLOUD_SYNC": "Sincronização na nuvem", - "CONTINUE_OFFLINE": "Usar offline", - "EMAIL": "E-mail", - "ERROR_MSGS": { - "DEFAULT_ERROR": "Não foi possível fazer o login, verifique as credenciais e tente novamente", - "INVALID_PASSWORD": "Senha inválida", - "TOO_MANY_ATTEMPTS_TRY_LATER": "Muitas tentativas de login. Tente novamente após 15 minutos", - "USER_NOT_FOUND": "Usuário não encontrado" - }, - "PASSWORD": "Senha", - "SIGN_IN_BUTTON": "Entrar", - "VALIDATION_MSGS": { - "EMAIL_PATTERN": "E-mail inválido", - "REQUIRED_EMAIL": "Obrigatório informar um e-mail", - "REQUIRED_PASSWORD": "Informe a senha" - } + "TRY_AGAIN": "Tentar novamente", + "ENCRYPTION_ALERT": { + "TITLE": "Alerta de segurança", + "MESSAGE": "Para melhor proteger suas contas, é recomendável ativar a funcionalidade de criptografia de chaves.

Deseja configurar uma senha agora?

", + "ENABLE_ENCRYPTION": "Ativar criptografia", + "LATER": "Mais tarde", + "DONT_SHOW_AGAIN": "Não mostrar novamente" + } + }, + "LOGIN": { + "AUTHENTICATING": "Autenticando...", + "CLOUD_SYNC": "Sincronização na nuvem", + "CONTINUE_OFFLINE": "Usar offline", + "EMAIL": "E-mail", + "ERROR_MSGS": { + "DEFAULT_ERROR": "Não foi possível fazer o login, verifique as credenciais e tente novamente", + "INVALID_PASSWORD": "Senha inválida", + "TOO_MANY_ATTEMPTS_TRY_LATER": "Muitas tentativas de login. Tente novamente após 15 minutos", + "USER_NOT_FOUND": "Usuário não encontrado" }, - "SEARCH": "Pesquisar", - "UPDATER": { - "UPDATE_AVAILABLE_BODY": "Uma nova versão da aplicação está disponível e será instalada automaticamente para obter as últimas funcionalidades e correções de bugs.", - "UPDATE_AVAILABLE_HEADER": "Atualização disponível", - "UPDATE_NOW": "Atualizar agora" + "PASSWORD": "Senha", + "SIGN_IN_BUTTON": "Entrar", + "VALIDATION_MSGS": { + "EMAIL_PATTERN": "E-mail inválido", + "REQUIRED_EMAIL": "Obrigatório informar um e-mail", + "REQUIRED_PASSWORD": "Informe a senha" } -} \ No newline at end of file + }, + "SEARCH": "Pesquisar", + "UPDATER": { + "UPDATE_AVAILABLE_BODY": "Uma nova versão da aplicação está disponível e será instalada automaticamente para obter as últimas funcionalidades e correções de bugs.", + "UPDATE_AVAILABLE_HEADER": "Atualização disponível", + "UPDATE_NOW": "Atualizar agora" + }, + "UTILS": { + "ERROR_INVALID_SEMVER_FORMAT": "Formato SemVer inválido" + } +} diff --git a/src/environments/environment.ts b/src/environments/environment.ts index 1289ee4..b0c0730 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -16,7 +16,7 @@ export const environment = { messagingSenderId: "946698001868" }, versionConfig: { - versionNumber: "%VERSION%", + versionNumber: "2.0.0", // set this to test migrations. On production build this will be the value from the package.json buildDate: "%BUILD_DATE%", commitHash: "%COMMIT_HASH%", versionName: "%VERSION%-DEV" diff --git a/src/global.scss b/src/global.scss index 053ab14..ed401c2 100644 --- a/src/global.scss +++ b/src/global.scss @@ -49,4 +49,8 @@ ion-toast.width-auto { ion-popover.wider-popover { --max-width: 300px; --width: 300px; +} + +ion-popover.wider-popover .version-info { + cursor: pointer; } \ No newline at end of file