From 71f6225e3cdddd4a17c0cc742871945f5de0b04d Mon Sep 17 00:00:00 2001 From: Alfonso Salces Date: Wed, 9 Aug 2023 15:05:17 +0200 Subject: [PATCH] MOBILE-4405 autologout: Create autologout service --- .../autologout/services/autologout.ts | 269 ++++++++++++++++++ src/core/features/compile/services/compile.ts | 2 + src/core/services/sites.ts | 27 ++ 3 files changed, 298 insertions(+) create mode 100644 src/core/features/autologout/services/autologout.ts diff --git a/src/core/features/autologout/services/autologout.ts b/src/core/features/autologout/services/autologout.ts new file mode 100644 index 00000000000..3c375356c6d --- /dev/null +++ b/src/core/features/autologout/services/autologout.ts @@ -0,0 +1,269 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { CoreSite } from '@classes/site'; +import { CorePlatform } from '@services/platform'; +import { CoreSites } from '@services/sites'; +import { CoreStorage, CoreStorageService } from '@services/storage'; +import { makeSingleton } from '@singletons'; +import { CoreEvents } from '@singletons/events'; +import { Subscription } from 'rxjs'; + +/** + * Auto logout service + */ +@Injectable({ providedIn: 'root' }) +export class CoreAutoLogoutService { + + /** + * Timestamp indicating the last time the application was in the foreground. + */ + protected static readonly TIMESTAMP_DB_KEY = 'CoreAutoLogoutTimestamp'; + + /** + * How often we will store a timestamp (in miliseconds). + */ + protected static readonly DEFAULT_TIMESTAMP_STORE_TIME = 10000; + + /** + * Grace period if you return to the application too soon (in miliseconds). + */ + protected static readonly GRACE_PERIOD = 30000; + + protected platformResumeSubscription?: Subscription; + protected platformPauseSubscription?: Subscription; + protected interval?: ReturnType; + protected backgroundTimestamp?: number; + + constructor() { + CoreEvents.on(CoreEvents.LOGIN, async() => await this.refreshListeners()); + CoreEvents.on(CoreEvents.LOGOUT, async() => { + this.cancelListeners(); + await CoreStorage.remove(CoreAutoLogoutService.TIMESTAMP_DB_KEY); + }); + } + + /** + * Refresh listeners for auto logout. + */ + async refreshListeners(): Promise { + if (!CoreSites.isLoggedIn()) { + return; + } + + const site = CoreSites.getCurrentSite(); + + if (!site) { + return; + } + + const autoLogoutType = Number(site.getStoredConfig('tool_mobile_autologout')); + this.cancelListeners(); + + if (!autoLogoutType || autoLogoutType === CoreAutoLogoutType.NEVER) { + return; + } + + if (autoLogoutType === CoreAutoLogoutType.CUSTOM) { + await this.setTimestamp(); + this.setInterval(); + } + + this.platformPauseSubscription = CorePlatform.pause.subscribe(async () => { + this.backgroundTimestamp = new Date().getTime(); + this.clearInterval(); + }); + + this.platformResumeSubscription = CorePlatform.resume.subscribe(async () => { + if (autoLogoutType !== CoreAutoLogoutType.CUSTOM) { + await this.handleAppClosed(site); + + return; + } + + const autoLogoutTime = Number(site.getStoredConfig('tool_mobile_autologouttime')); + const loggedOut = await this.handleSessionClosed(autoLogoutTime, site); + + if (!loggedOut) { + await this.setTimestamp(); + this.setInterval(); + } + }); + } + + /** + * Set site logged out. + * + * @param siteId site id. + */ + protected async logout(siteId: string): Promise { + await CoreSites.setSiteLoggedOut(siteId, true); + } + + /** + * Saves stored timestamp. + */ + protected async setTimestamp(): Promise { + const date = new Date().getTime(); + await CoreStorageService.forCurrentSite().set(CoreAutoLogoutService.TIMESTAMP_DB_KEY, date); + } + + /** + * Gives if auto logout can be displayed. + * + * @returns true if can display, false if not. + */ + async canShowPreference(): Promise { + const site = CoreSites.getCurrentSite(); + + if (!site) { + return false; + } + + const autoLogoutType = Number(site.getStoredConfig('tool_mobile_autologout')); + + return autoLogoutType !== CoreAutoLogoutType.NEVER; + } + + /** + * Cancel uncompleted listeners. + */ + protected cancelListeners(): void { + this.clearInterval(); + this.platformResumeSubscription?.unsubscribe(); + this.platformPauseSubscription?.unsubscribe(); + delete this.platformPauseSubscription; + delete this.platformResumeSubscription; + } + + /** + * Set interval. + */ + protected setInterval(): void { + this.interval = setInterval(async () => await this.setTimestamp(), CoreAutoLogoutService.DEFAULT_TIMESTAMP_STORE_TIME); + } + + /** + * Clear interval. + */ + protected clearInterval(): void { + if (!this.interval) { + return; + } + + clearInterval(this.interval); + delete this.interval; + } + + /** + * Logout user if his session is expired. + * + * @param sessionDuration Session duration. + * @param site Current site. + * @returns Whether site has been logged out. + */ + async handleSessionClosed(sessionDuration: number, site: CoreSite): Promise { + if (!site.id) { + return false; + } + + const storage = CoreStorageService.forSite(site); + const savedTimestamp = await storage.get(CoreAutoLogoutService.TIMESTAMP_DB_KEY); + + if (!savedTimestamp) { + return false; + } + + // Get expiration time from site preferences as miliseconds. + const expirationDate = savedTimestamp + ((sessionDuration || 0) * 1000); + await storage.remove(CoreAutoLogoutService.TIMESTAMP_DB_KEY); + + if (new Date().getTime() < expirationDate) { + return false; + } + + await this.logout(site.id); + + return true; + } + + /** + * Logout if user closed the app. + * + * @param site Current site. + * @returns Whether site has been logged out. + */ + async handleAppClosed(site: CoreSite): Promise { + if (!site.id) { + return false; + } + + if ( + this.backgroundTimestamp && + (this.backgroundTimestamp + CoreAutoLogoutService.GRACE_PERIOD) > new Date().getTime() + ) { + delete this.backgroundTimestamp; + + return false; + } + + await this.logout(site.id); + + return true; + } + + getConfig(): { autoLogoutType: CoreAutoLogoutType; autoLogoutTime: number } { + const site = CoreSites.getRequiredCurrentSite(); + const autoLogoutType = Number(site.getStoredConfig('tool_mobile_autologout')); + const autoLogoutTime = Number(site.getStoredConfig('tool_mobile_autologouttime')); + + return { autoLogoutType, autoLogoutTime }; + } + +} + +export type CoreAutoLogoutSessionConfig = { + type: CoreAutoLogoutType.CUSTOM; + sessionDuration: number; +}; + +export type CoreAutoLogoutOtherConfig = { + type: Exclude; +}; + +/** + * Possible automatic logout cases. + */ +export enum CoreAutoLogoutType { + /** + * Disabled automatic logout. + */ + NEVER = 0, + + /** + * When the user closes the app, in next login he need to login again. + */ + INMEDIATE = 1, + + /** + * This applies when session time is set. If the user closes the app more time than the specified, + * then, the user must login again. + */ + CUSTOM = 2, +}; + +export type CoreAutoLogoutConfig = CoreAutoLogoutSessionConfig | CoreAutoLogoutOtherConfig; + +export const CoreAutoLogout = makeSingleton(CoreAutoLogoutService); diff --git a/src/core/features/compile/services/compile.ts b/src/core/features/compile/services/compile.ts index 0ba9a739c20..5f207b95b0c 100644 --- a/src/core/features/compile/services/compile.ts +++ b/src/core/features/compile/services/compile.ts @@ -158,6 +158,7 @@ import { AddonModAssignComponentsModule } from '@addons/mod/assign/components/co import { AddonModWorkshopComponentsModule } from '@addons/mod/workshop/components/components.module'; import { CorePromisedValue } from '@classes/promised-value'; import { CorePlatform } from '@services/platform'; +import { CoreAutoLogoutService } from '@features/autologout/services/autologout'; /** * Service to provide functionalities regarding compiling dynamic HTML and Javascript. @@ -263,6 +264,7 @@ export class CoreCompileProvider { injectLibraries(instance: any, extraProviders: Type[] = []): void { const providers = [ ...CORE_SERVICES, + CoreAutoLogoutService, ...CORE_BLOCK_SERVICES, ...CORE_COMMENTS_SERVICES, ...CORE_CONTENTLINKS_SERVICES, diff --git a/src/core/services/sites.ts b/src/core/services/sites.ts index fed018ceebe..06fbcecb009 100644 --- a/src/core/services/sites.ts +++ b/src/core/services/sites.ts @@ -63,6 +63,7 @@ import { CoreConfig } from './config'; import { CoreNetwork } from '@services/network'; import { CoreUserGuestSupportConfig } from '@features/user/classes/support/guest-support-config'; import { CoreLang, CoreLangFormat } from '@services/lang'; +import { CoreAutoLogoutType, CoreAutoLogout } from '@features/autologout/services/autologout'; export const CORE_SITE_SCHEMAS = new InjectionToken('CORE_SITE_SCHEMAS'); export const CORE_SITE_CURRENT_SITE_ID_CONFIG = 'current_site_id'; @@ -1421,6 +1422,8 @@ export class CoreSitesProvider { * @returns Promise resolved if a session is restored. */ async restoreSession(): Promise { + await this.handleAutoLogout(); + if (this.sessionRestored) { return Promise.reject(new CoreError('Session already restored.')); } @@ -1437,6 +1440,30 @@ export class CoreSitesProvider { } } + /** + * Handle auto logout by checking autologout type and time if its required. + */ + async handleAutoLogout(): Promise { + await CoreUtils.ignoreErrors(( async () => { + const siteId = await this.getStoredCurrentSiteId(); + const site = await this.getSite(siteId); + const autoLogoutType = Number(site.getStoredConfig('tool_mobile_autologout')); + const autoLogoutTime = Number(site.getStoredConfig('tool_mobile_autologouttime')); + + if (autoLogoutType === CoreAutoLogoutType.NEVER || !site.id) { + return; + } + + if (autoLogoutType === CoreAutoLogoutType.CUSTOM) { + await CoreAutoLogout.handleSessionClosed(autoLogoutTime, site); + + return; + } + + await CoreAutoLogout.handleAppClosed(site); + })()); + } + /** * Mark a site as logged out so the user needs to authenticate again. *