Skip to content

Improve offline behavior #20

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Oct 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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