Skip to content

Commit

Permalink
Improve offline behavior (#20)
Browse files Browse the repository at this point in the history
* Improve offline behavior
Fixes #16

* Improve account loading flow in remote service

- Fix last selected account not restoring on app reopen #19

* improve account lock logic and migrations

* Update version

* test action context info

* update actions to get proper commit sha
  • Loading branch information
jlcvp authored Oct 10, 2024
1 parent 094130f commit 7b0e7de
Show file tree
Hide file tree
Showing 14 changed files with 159 additions and 119 deletions.
16 changes: 16 additions & 0 deletions .github/actions/prepare-sha/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# .github/actions/prepare-sha/action.yml
name: 'Prepare Commit SHA'
description: ''
runs:
using: "composite"
steps:
- name: Setup Environment (PR)
if: ${{ github.event_name == 'pull_request' }}
shell: bash
run: |
echo "LAST_COMMIT_SHA=${{ github.event.pull_request.head.sha }}" >> ${GITHUB_ENV}
- name: Setup Environment (Push)
if: ${{ github.event_name == 'push' }}
shell: bash
run: |
echo "LAST_COMMIT_SHA=${GITHUB_SHA}" >> ${GITHUB_ENV}
2 changes: 2 additions & 0 deletions .github/workflows/deploy_gh_pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Prepare Commit SHA
uses: ./.github/actions/prepare-sha
- name: Build
run: npm ci && npm run build:githubpages
- name: Create 404 page
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/firebase-hosting-merge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Prepare Commit SHA
uses: ./.github/actions/prepare-sha
- run: npm ci && npm run build
- uses: FirebaseExtended/action-hosting-deploy@v0
with:
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/firebase-hosting-pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Prepare Commit SHA
uses: ./.github/actions/prepare-sha
- run: npm ci && npm run build
- uses: FirebaseExtended/action-hosting-deploy@v0
with:
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "authleu",
"version": "2.0.0",
"version": "2.1.0",
"author": "Leonardo 'Leu' Pereira <jlcvp@users.noreply.github.com>",
"homepage": "https://github.com/jlcvp/AuthLeu",
"description": "Open source authenticator and 2fa code generator to use across multiple devices and platforms",
Expand Down
4 changes: 2 additions & 2 deletions resources/scripts/version-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ const replacer = require('replace-in-file')
const package = require('../../package.json')

const buildDate = new Date().toISOString()
// get git commit hash from GITHUB_SHA environment variable
const commitHash = process.env.GITHUB_SHA || 'unknown'
// get git commit hash from LAST_COMMIT_SHA environment variable set in the prepare action
const commitHash = process.env.LAST_COMMIT_SHA || 'unknown'

const options = {
files: 'src/environments/environment*.ts',
Expand Down
137 changes: 47 additions & 90 deletions src/app/home/home.page.ts
Original file line number Diff line number Diff line change
@@ -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 { firstValueFrom, Observable, tap } from 'rxjs';
import { Account2FA } from '../models/account2FA.model';
import { Account2faService } from '../services/accounts/account2fa.service';
import { LogoService } from '../services/logo.service';
Expand Down Expand Up @@ -54,7 +54,6 @@ export class HomePage implements OnInit {

accounts$: Observable<Account2FA[]> = new Observable<Account2FA[]>();
selectedAccount?: Account2FA
lockedAccount?: Account2FA
searchTxt: string = ''
draftLogoSearchTxt: string = ''
searchLogoResults: any[] = []
Expand All @@ -66,13 +65,14 @@ export class HomePage implements OnInit {
isAddAccountModalOpen: boolean = false
isScanActive: boolean = false
isWindowFocused: boolean = true
hasLockedAccounts: boolean = false
versionInfo
hasLockedAccounts: boolean = true
versionInfo: any

private encryptionOptions: EncryptionOptions = ENCRYPTION_OPTIONS_DEFAULT
private systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)');
private isLandscape: boolean = false
private currentDarkModePref: string = '';
private shouldAlertAboutLockedAccounts: boolean = true
constructor(
private authService: AuthenticationService,
private accountsService: Account2faService,
Expand Down Expand Up @@ -141,18 +141,13 @@ export class HomePage implements OnInit {
return this.encryptionOptions.shouldPerformPeriodicCheck
}

get shouldAlertToActivateEncryption() {
return this.encryptionOptions.shouldAlertToActivateEncryption
}

async ngOnInit() {
await this.migrationService.migrate()
this.onWindowResize()
this.setupPalette()
GlobalUtils.hideSplashScreen()
await this.setupEncryption()
await this.loadAccounts()
await this.handleEncryptionReminder()
await this.configService.setFirstRun(false)
}

Expand Down Expand Up @@ -316,8 +311,8 @@ export class HomePage implements OnInit {
const selectedAccounts = data ? data as Account2FA[] : undefined
if (selectedAccounts && selectedAccounts.length > 0) {
// check if imported accounts are encrypted
const lockedAccount = selectedAccounts.find(account => account.isEncrypted)
if (lockedAccount !== undefined) {
const lockedAccounts = selectedAccounts.filter(account => account.isEncrypted)
if (lockedAccounts.length > 0) {
const password = await this.configService.getEncryptionKey()
let currentPasswordWorks = false;
if (password) {
Expand All @@ -338,7 +333,7 @@ export class HomePage implements OnInit {
if (!currentPasswordWorks) {
try {
// ask for password
const password = await this.promptUnlockPassword(lockedAccount)
const password = await this.promptUnlockPassword(lockedAccounts)
if (!password) {
const message = await firstValueFrom(this.translateService.get('ACCOUNT_SYNC.ERROR.NO_PASSWORD_PROVIDED'))
throw new Error(message)
Expand Down Expand Up @@ -395,6 +390,7 @@ export class HomePage implements OnInit {
}

async unlockAccountsAction() {
this.shouldAlertAboutLockedAccounts = true
await this.loadAccounts()
}

Expand Down Expand Up @@ -662,25 +658,19 @@ export class HomePage implements OnInit {
backdropDismiss: false
})
await loading.present()
const accounts$ = await this.accountsService.getAccounts()

// detect if there are locked accounts and call activate encryption flow
const accounts = await firstValueFrom(accounts$)
const lockedAccount = accounts.find(account => account.isLocked)
await loading.dismiss()
if(lockedAccount) {
this.hasLockedAccounts = true
if(await this.alertAccountsLocked()) { // user wants to informPassword
const password = await this.promptUnlockPassword(lockedAccount)
if(password) { // user provided the correct password
// save password and enable encryption
await this.configService.setEncryptionKey(password)
await this.configService.setLastPasswordCheck()
await this.setEncryptionActive(true)
this.hasLockedAccounts = false
}
const accounts$ = (await this.accountsService.getAccounts()).pipe(tap(accounts => {
const lockedAccounts = accounts.filter(account => account.isLocked)
this.hasLockedAccounts = lockedAccounts.length > 0
if(this.shouldAlertAboutLockedAccounts && this.hasLockedAccounts) {
this.shouldAlertAboutLockedAccounts = false
this.handleAccountsLocked(lockedAccounts)
}
}
this.handleAccountSelection(accounts)
console.log("Accounts tapped", { accounts })
}))

await loading.dismiss()

this.accounts$ = accounts$
}
Expand Down Expand Up @@ -756,7 +746,6 @@ export class HomePage implements OnInit {
private async setEncryptionActive(active: boolean) {
this.encryptionOptions.encryptionActive = active
this.encryptionOptions.shouldPerformPeriodicCheck = active
this.encryptionOptions.shouldAlertToActivateEncryption = !active
await this.saveEncryptionOptions()
}

Expand Down Expand Up @@ -821,63 +810,6 @@ export class HomePage implements OnInit {
return data.values
}

private async alertToActivateEncryption(): Promise<boolean> {
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) { // 2.1.2
this.encryptionOptions.shouldAlertToActivateEncryption = false
await this.saveEncryptionOptions()
}

return role === 'enable'
}

private async handleEncryptionReminder() {
const isFirstLaunch = await this.configService.isFirstRun()
if (this.shouldAlertToActivateEncryption && !isFirstLaunch) { // 2.1
const shouldEnableEncryption = await this.alertToActivateEncryption()
if(shouldEnableEncryption) { // 2.1.1
await this.activateEncryption()
return await this.setupEncryption()
}
}
}

private async periodicPasswordCheck() {
const lastCheck = await this.configService.getLastPasswordCheck()
const nextCheck = lastCheck + PASSWORD_CHECK_PERIOD
Expand Down Expand Up @@ -977,8 +909,8 @@ export class HomePage implements OnInit {
return false
}

private async promptUnlockPassword(lockedAccount: Account2FA): Promise<string> {
if(!lockedAccount.isLocked) {
private async promptUnlockPassword(lockedAccounts: Account2FA[]): Promise<string> {
if(!lockedAccounts.some(account => account.isLocked)) {
const message = await firstValueFrom(this.translateService.get('HOME.ASK_PASSWORD.ERROR_NOT_LOCKED'))
throw new Error(message)
}
Expand All @@ -992,7 +924,9 @@ export class HomePage implements OnInit {
break
} else {
try {
await lockedAccount.unlock(password)
for(const lockedAccount of lockedAccounts) {
await lockedAccount.unlock(password)
}
success = true
} catch (error) {
const message = await firstValueFrom(this.translateService.get('HOME.ERRORS.INVALID_PASSWORD'))
Expand Down Expand Up @@ -1041,6 +975,29 @@ export class HomePage implements OnInit {
return ''
}

private async handleAccountsLocked(lockedAccounts: Account2FA[]): Promise<void> {
if(await this.alertAccountsLocked()) { // user wants to informPassword
const password = await this.promptUnlockPassword(lockedAccounts)
if(password) { // user provided the correct password
// save password and enable encryption
await this.configService.setEncryptionKey(password)
await this.configService.setLastPasswordCheck()
await this.setEncryptionActive(true)
this.hasLockedAccounts = false
}
}
}

private async handleAccountSelection(accounts: Account2FA[]): Promise<void> {
const lastSelectedAccountId = await this.storageService.get<string>('lastSelectedAccountId')
if (lastSelectedAccountId) {
const selectedAccount = accounts.find(account => account.id === lastSelectedAccountId)
if (selectedAccount) {
this.selectAccount(selectedAccount)
}
}
}

private async alertAccountsLocked(): Promise<boolean> {
const title = await firstValueFrom(this.translateService.get('HOME.ERRORS.ACCOUNTS_LOCKED_TITLE'))
const message = await firstValueFrom(this.translateService.get('HOME.ERRORS.ACCOUNTS_LOCKED'))
Expand Down
1 change: 1 addition & 0 deletions src/app/models/app-version.enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ export enum AppVersion {
UNKNOWN = 'UNKNOWN',
V1_0_0 = '1.0.0',
V2_0_0 = '2.0.0',
V2_1_0 = '2.1.0'
}
4 changes: 1 addition & 3 deletions src/app/models/encryption-options.model.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
export interface EncryptionOptions {
encryptionActive: boolean;
shouldPerformPeriodicCheck: boolean;
shouldAlertToActivateEncryption: boolean;
}

export const ENCRYPTION_OPTIONS_KEY = 'encryptionOptions';
export const ENCRYPTION_OPTIONS_PASSWORD_KEY = '_eok';
export const ENCRYPTION_OPTIONS_DEFAULT: EncryptionOptions = {
encryptionActive: false,
shouldPerformPeriodicCheck: true,
shouldAlertToActivateEncryption: true
shouldPerformPeriodicCheck: true
};
export const LAST_PASSWORD_CHECK_KEY = 'lastPasswordCheck';
export const PASSWORD_CHECK_PERIOD = 1000 * 60 * 60 * 24 * 7; // 7 days
13 changes: 8 additions & 5 deletions src/app/services/accounts/local-account2fa.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,14 @@ export class LocalAccount2faService implements IAccount2FAProvider {
return this.updateAccountsBatch([account])
}

async updateAccountsBatch(accounts: Account2FA[]): Promise<void> {
await this.loadAccountsFromStorage()
for (const account of accounts) {
this.updateAccountData(account)
async updateAccountsBatch(accounts: Account2FA[], replaceExisting: boolean = false): Promise<void> {
if(replaceExisting) {
this.accounts = accounts
} else {
await this.loadAccountsFromStorage()
for (const account of accounts) {
this.updateAccountData(account)
}
}
this.persistAccounts()
this.accountsSubject.next(this.accounts)
Expand All @@ -76,7 +80,6 @@ export class LocalAccount2faService implements IAccount2FAProvider {
const accounts = (await this.localStorage.get<IAccount2FA[]>('local_accounts')) || []
this.accounts = accounts.map(account => Account2FA.fromDictionary(account))
this.sortAccounts()
this.accountsSubject.next(this.accounts)
}

private createId(): string {
Expand Down
Loading

0 comments on commit 7b0e7de

Please sign in to comment.