import { Component , OnInit } from 'angular-ts-decorators' ; import autobind from 'autobind-decorator' ; import * as countriesList from 'countries-list' ; import { ApiServiceStatus } from '../../shared/api/api.enum' ; import { ApiService , ApiServiceInfo } from '../../shared/api/api.interface' ; import { CryptoService } from '../../shared/crypto/crypto.service' ; import { BaseError , InvalidCredentialsError , InvalidServiceError , SyncVersionNotSupportedError , UnsupportedApiVersionError } from '../../shared/errors/errors' ; import { ExceptionHandler } from '../../shared/errors/errors.interface' ; import Globals from '../../shared/global-shared.constants' ; import { PlatformType } from '../../shared/global-shared.enum' ; import { PlatformService } from '../../shared/global-shared.interface' ; import { LogService } from '../../shared/log/log.service' ; import { StoreKey } from '../../shared/store/store.enum' ; import { StoreService } from '../../shared/store/store.service' ; import { SyncType } from '../../shared/sync/sync.enum' ; import { Sync } from '../../shared/sync/sync.interface' ; import { UtilityService } from '../../shared/utility/utility.service' ; import { WorkingService } from '../../shared/working/working.service' ; import { RoutePath } from '../app.enum' ; import { AppHelperService } from '../shared/app-helper/app-helper.service' ; @ autobind @ Component ( { controllerAs : 'vm' , selector : 'appLogin' , styles : [ require ( './app-login.component.scss' ) ] , template : require ( './app-login.component.html' ) } ) export class AppLoginComponent implements OnInit { Strings = require ( '../../../../res/strings/en.json' ) ; $exceptionHandler : ExceptionHandler ; $q : ng . IQService ; $timeout : ng . ITimeoutService ; apiSvc : ApiService ; appHelperSvc : AppHelperService ; cryptoSvc : CryptoService ; logSvc : LogService ; platformSvc : PlatformService ; storeSvc : StoreService ; utilitySvc : UtilityService ; workingSvc : WorkingService ; ApiServiceStatus = ApiServiceStatus ; displayGetSyncIdPanel : boolean ; displayOtherSyncsWarning = false ; displayPasswordConfirmation = false ; enablePasswordValidation = false ; displaySyncConfirmation = false ; displayUpdateServiceConfirmation = false ; displayUpdateServicePanel = false ; displayUpgradeConfirmation = false ; newServiceInfo : ApiServiceInfo ; newSync = false ; password : string ; passwordComplexity : any ; passwordConfirmation = null ; platformType = PlatformType ; serviceInfo : ApiServiceInfo ; showPassword = false ; syncEnabled = false ; syncForm : ng . IFormController ; syncId : string ; upgradeConfirmed = false ; validatingServiceUrl = false ; static $inject = [ '$exceptionHandler' , '$q' , '$timeout' , 'ApiService' , 'AppHelperService' , 'CryptoService' , 'LogService' , 'PlatformService' , 'StoreService' , 'UtilityService' , 'WorkingService' ] ; constructor ( $exceptionHandler , $q , $timeout , ApiSvc , AppHelperSvc , CryptoSvc , LogSvc , PlatformSvc , StoreSvc , UtilitySvc , WorkingSvc ) { this . $exceptionHandler = $exceptionHandler ; this . $q = $q ; this . $timeout = $timeout ; this . apiSvc = ApiSvc ; this . appHelperSvc = AppHelperSvc ; this . cryptoSvc = CryptoSvc ; this . logSvc = LogSvc ; this . platformSvc = PlatformSvc ; this . storeSvc = StoreSvc ; this . utilitySvc = UtilitySvc ; this . workingSvc = WorkingSvc ; } cancelConfirmPassword ( ) { this . displayPasswordConfirmation = false ; this . passwordConfirmation = null ; } cancelUpdateService ( ) { this . displayUpdateServiceConfirmation = false ; this . displayUpdateServicePanel = false ; } confirmPassword ( ) { this . displayPasswordConfirmation = true ; this . appHelperSvc . focusOnElement ( 'input[name="txtPasswordConfirmation"]' ) ; } confirmUpdateService ( ) { const url = this . newServiceInfo . url . replace ( /\/$/ , '' ) ; this . appHelperSvc . updateServiceUrl ( url ) . then ( ( serviceInfo ) => { this . serviceInfo = serviceInfo ; this . serviceInfo . url = url ; return this . $q . all ( [ this . storeSvc . remove ( StoreKey . SyncId ) , this . storeSvc . remove ( StoreKey . Password ) ] ) ; } ) . then ( ( ) => { this . displayUpdateServiceConfirmation = false ; this . displayUpdateServicePanel = false ; this . password = undefined ; this . passwordComplexity = undefined ; this . passwordConfirmation = undefined ; this . syncId = undefined ; this . syncForm . txtId . $setValidity ( 'InvalidSyncId' , true ) ; this . syncForm . $setPristine ( ) ; this . syncForm . $setUntouched ( ) ; this . appHelperSvc . focusOnElement ( '.active-login-form input' ) ; } ) . catch ( ( err ) => this . logSvc . logError ( err ) ) ; } returns currentLocaleSupportsPasswordValidation ( ) { return this . platformSvc . getCurrentLocale ( ) . then ( ( currentLocale ) => { return currentLocale . indexOf ( 'en' ) === 0 || currentLocale . indexOf ( 'de' ) === 0 ; } ) ; } disableSync ( ) { return this . platformSvc . disableSync ( ) . then ( ( ) => { this . syncEnabled = false ; this . password = undefined ; this . passwordComplexity = undefined ; } ) ; } dismissOtherSyncsWarning ( ) { this . displayOtherSyncsWarning = false ; this . storeSvc . set ( StoreKey . DisplayOtherSyncsWarning , false ) ; this . appHelperSvc . focusOnElement ( '.active-login-form input[name="txtPassword"]' ) ; } displayExistingSyncPanel ( event ? ) { event ?. preventDefault ( ) ; this . newSync = false ; this . password = undefined ; this . appHelperSvc . focusOnElement ( 'input[name="txtId"]' ) ; } displayNewSyncPanel ( event ? ) { event ?. preventDefault ( ) ; this . newSync = true ; this . displayPasswordConfirmation = false ; this . password = undefined ; this . passwordComplexity = undefined ; this . passwordConfirmation = undefined ; this . storeSvc . remove ( StoreKey . SyncId ) ; this . storeSvc . remove ( StoreKey . Password ) ; this . syncId = undefined ; this . syncForm . txtId . $setValidity ( 'InvalidSyncId' , true ) ; this . appHelperSvc . focusOnElement ( '.login-form-new input[name="txtPassword"]' ) ; } enableManualEntry ( event ? ) { event ?. preventDefault ( ) ; this . displayGetSyncIdPanel = false ; } executeSync ( ) { const syncData = { type : SyncType . Local } ; let syncInfoMessage ; this . displaySyncConfirmation = false ; this . displayOtherSyncsWarning = false ; this . displayUpgradeConfirmation = false ; this . workingSvc . show ( ) ; this . apiSvc . checkServiceStatus ( ) . then ( ( ) => this . storeSvc . remove ( StoreKey . Password ) ) . then ( ( ) => { if ( ! this . syncId ) { syncData . type = SyncType . Remote ; return this . apiSvc . createNewSync ( ) . then ( ( newSync ) => { syncInfoMessage = `New sync id created: ${ newSync . id }` ; return this . $q . all ( [ this . storeSvc . set ( StoreKey . LastUpdated , newSync . lastUpdated ) , this . storeSvc . set ( StoreKey . SyncId , newSync . id ) , this . storeSvc . set ( StoreKey . SyncVersion , newSync . version ) ] ) . then ( ( ) => newSync . id ) ; } ) ; } return this . $q . all ( [ this . apiSvc . getBookmarksVersion ( this . syncId ) , this . platformSvc . getAppVersion ( ) ] ) . then ( ( results ) => { const [ response , appVersion ] = results ; const { version : bookmarksVersion } = response ; if ( this . utilitySvc . compareVersions ( bookmarksVersion ?? '0' , appVersion , '<' ) ) { if ( this . upgradeConfirmed ) { syncData . type = SyncType . Upgrade ; } else { this . displayUpgradeConfirmation = true ; return ; } } else if ( this . utilitySvc . compareVersions ( bookmarksVersion ?? '0' , appVersion , '>' ) ) { throw new SyncVersionNotSupportedError ( ) ; } syncInfoMessage = `Synced to existing id: ${ this . syncId }` ; return this . $q . all ( [ this . storeSvc . set ( StoreKey . SyncId , this . syncId ) , this . storeSvc . set ( StoreKey . SyncVersion , bookmarksVersion ) ] ) . then ( ( ) => this . syncId ) ; } ) ; } ) . then ( ( syncId ) => { if ( ! syncId ) { return ; } return this . cryptoSvc . getPasswordHash ( this . password , syncId ) . then ( ( passwordHash ) => this . storeSvc . set ( StoreKey . Password , passwordHash ) . then ( ( ) => this . platformSvc . queueSync ( syncData ) ) ) . then ( ( ) => { this . logSvc . logInfo ( syncInfoMessage ) ; return this . appHelperSvc . syncBookmarksSuccess ( ) ; } ) . then ( ( ) => { this . syncEnabled = true ; this . syncId = syncId ; } ) . catch ( ( err ) => this . syncFailed ( err , syncData ) ) ; } ) . catch ( ( err ) => { this . upgradeConfirmed = false ; throw err ; } ) . finally ( ( ) => this . workingSvc . hide ( ) ) ; } getCountryNameFrom2LetterISOCode ( isoCode ) { if ( ! isoCode ) { return ; } const country = countriesList . countries [ isoCode ] ; if ( ! country ) { this . logSvc . logWarning ( `No country found matching ISO code: ${ isoCode }` ) ; } return country . name ; } getServiceStatusTextFromStatusCode ( statusCode ) { switch ( statusCode ) { case ApiServiceStatus . NoNewSyncs : return this . platformSvc . getI18nString ( this . Strings . Service . Status . NoNewSyncs ) ; case ApiServiceStatus . Offline : return this . platformSvc . getI18nString ( this . Strings . Service . Status . Offline ) ; case ApiServiceStatus . Online : return this . platformSvc . getI18nString ( this . Strings . Service . Status . Online ) ; case ApiServiceStatus . Error : default : return this . platformSvc . getI18nString ( this . Strings . Service . Status . Error ) ; } } ngOnInit ( ) { this . $q . all ( [ this . storeSvc . get ( [ StoreKey . DisplayOtherSyncsWarning , StoreKey . SyncId ] ) , this . utilitySvc . isSyncEnabled ( ) , this . utilitySvc . getServiceUrl ( ) , this . currentLocaleSupportsPasswordValidation ( ) ] ) . then ( ( data ) => { const [ storeContent , syncEnabled , serviceUrl , currentLocaleIsEnglish ] = data ; this . displayOtherSyncsWarning = storeContent . displayOtherSyncsWarning ; this . syncId = storeContent . syncId ; this . syncEnabled = syncEnabled ; this . enablePasswordValidation = currentLocaleIsEnglish ; this . serviceInfo = { url : serviceUrl } ; if ( this . syncId ) { this . $timeout ( ( ) => this . validateSyncId ( ) , Globals . InterfaceReadyTimeout ) ; } if ( this . utilitySvc . isMobilePlatform ( this . platformSvc . platformName ) ) { this . displayGetSyncIdPanel = ! this . syncId ; this . newSync = false ; } else { this . newSync = ! this . syncId ; if ( this . displayOtherSyncsWarning ) { this . appHelperSvc . focusOnElement ( '.otherSyncsWarning .buttons > button' ) ; } else if ( ! this . utilitySvc . isMobilePlatform ( this . platformSvc . platformName ) ) { this . appHelperSvc . focusOnElement ( '.active-login-form  input[name="txtPassword"]' ) ; } } this . refreshServiceStatus ( ) . then ( ( ) => this . appHelperSvc . attachClickEventsToNewTabLinks ( ) ) ; } ) ; } refreshServiceStatus ( ) { return this . appHelperSvc . formatServiceInfo ( ) . then ( ( formattedServiceInfo ) => { Object . assign ( this . serviceInfo , formattedServiceInfo ) ; } ) ; } scanId ( event ? ) { event ?. preventDefault ( ) ; this . appHelperSvc . switchView ( RoutePath . Scan ) ; } serviceIsOnline ( ) { return ( this . serviceInfo . status === ApiServiceStatus . NoNewSyncs || this . serviceInfo . status === ApiServiceStatus . Online ) ; } serviceUrlChanged ( ) { if ( this . syncForm . newServiceUrl . $invalid ) { this . syncForm . newServiceUrl . $setValidity ( 'InvalidService' , true ) ; this . syncForm . newServiceUrl . $setValidity ( 'InvalidUrl' , true ) ; this . syncForm . newServiceUrl . $setValidity ( 'RequestFailed' , true ) ; this . syncForm . newServiceUrl . $setValidity ( 'ServiceVersionNotSupported' , true ) ; } if ( ( this . newServiceInfo . url ?? undefined ) === undefined ) { return ; } if ( ! new RegExp ( `^${ Globals . URL . ValidUrlRegex }$` , 'i' ) . test ( this . newServiceInfo . url ) ) { this . syncForm . newServiceUrl . $setValidity ( 'InvalidUrl' , false ) ; } } submitForm ( ) { this . $timeout ( ( ) => { if ( this . displayUpdateServicePanel ) { ( document . querySelector ( '.update-service-panel .btn-update-service-url' ) as HTMLButtonElement ) . click ( ) ; } else if ( this . newSync ) { if ( this . displayPasswordConfirmation ) { ( document . querySelector ( '.login-form-new .btn-new-sync' ) as HTMLButtonElement ) . click ( ) ; } else { ( document . querySelector ( '.login-form-new .btn-confirm-password' ) as HTMLButtonElement ) . click ( ) ; } } else { ( document . querySelector ( '.login-form-existing .btn-existing-sync' ) as HTMLButtonElement ) . click ( ) ; } } ) ; } switchService ( ) { this . newServiceInfo = { url : this . serviceInfo . url } ; this . displayUpdateServiceConfirmation = false ; this . displayUpdateServicePanel = true ; this . validatingServiceUrl = false ; this . validateServiceUrl ( ) . finally ( ( ) => this . appHelperSvc . focusOnElement ( '.update-service-panel input' ) ) ; } switchToHelpView ( ) { this . appHelperSvc . switchView ( RoutePath . Help ) ; } switchToSettingsView ( ) { this . appHelperSvc . switchView ( RoutePath . Settings ) ; } sync ( ) { if ( this . syncId && this . appHelperSvc . confirmBeforeSyncing ( ) ) { this . displaySyncConfirmation = true ; this . appHelperSvc . focusOnElement ( '.btn-confirm-enable-sync' ) ; } else { this . executeSync ( ) ; } } syncFailed ( err , sync ) { this . upgradeConfirmed = false ; const keys = [ StoreKey . Bookmarks , StoreKey . Password , StoreKey . SyncVersion ] ; if ( sync . type === SyncType . Remote ) { keys . push ( StoreKey . SyncId ) ; } this . storeSvc . remove ( keys ) . then ( ( ) => { if ( err instanceof InvalidCredentialsError && ! this . utilitySvc . isMobilePlatform ( this . platformSvc . platformName ) ) { this . $timeout ( ( ) => { ( document . querySelector ( '.login-form-existing input[name="txtPassword"]' ) as HTMLInputElement ) . select ( ) ; } , Globals . InterfaceReadyTimeout ) ; } throw err ; } ) ; } syncIdChanged ( ) { if ( this . validateSyncId ( ) ) { this . storeSvc . set ( StoreKey . SyncId , this . syncId ) ; } } toggleShowPassword ( ) { this . showPassword = ! this . showPassword ; } updateServiceUrl ( ) { if ( this . newServiceInfo . url ?. trim ( ) && ! new RegExp ( Globals . URL . ProtocolRegex ) . test ( this . newServiceInfo . url ?? '' ) ) { this . newServiceInfo . url = `https://${ this . newServiceInfo . url }` ; } this . validateServiceUrl ( ) . then ( ( newServiceInfo ) => { if ( ! newServiceInfo ) { return ; } return this . appHelperSvc . formatServiceInfo ( newServiceInfo ) . then ( ( serviceInfo ) => { Object . assign ( this . newServiceInfo , serviceInfo ) ; this . displayUpdateServiceConfirmation = true ; this . appHelperSvc . attachClickEventsToNewTabLinks ( document . querySelector ( '.service-message' ) ) ; this . appHelperSvc . focusOnElement ( '.focused' ) ; } ) ; } ) ; } upgradeSync ( ) { this . upgradeConfirmed = true ; this . executeSync ( ) ; } validateServiceUrl ( ) { this . validatingServiceUrl = true ; const url = this . newServiceInfo . url . replace ( /\/$/ , '' ) ; return this . apiSvc . checkServiceStatus ( url ) . catch ( ( err ) => { switch ( err . constructor ) { case UnsupportedApiVersionError : this . syncForm . newServiceUrl . $setValidity ( 'ServiceVersionNotSupported' , false ) ; break ; case InvalidServiceError : this . syncForm . newServiceUrl . $setValidity ( 'InvalidService' , false ) ; break ; default : this . syncForm . newServiceUrl . $setValidity ( 'RequestFailed' , false ) ; } this . appHelperSvc . focusOnElement ( 'input[name=newServiceUrl]' , true ) ; } ) . finally ( ( ) => { this . validatingServiceUrl = false ; } ) ; } validateSyncId ( ) { const isValid = ! this . syncId || this . utilitySvc . syncIdIsValid ( this . syncId ) ; this . syncForm . txtId . $setValidity ( 'InvalidSyncId' , isValid ) ; return isValid ; } }