forked from EVOgeek/Vmeda.Online
		
	MOBILE-3689 init: Replace /login/init with guards
This commit is contained in:
		
							parent
							
								
									033860d18b
								
							
						
					
					
						commit
						2a5e29b1c3
					
				| @ -26,6 +26,7 @@ import { | ||||
| } from '@angular/router'; | ||||
| 
 | ||||
| import { CoreArray } from '@singletons/array'; | ||||
| import { CoreRedirectGuard } from '@guards/redirect'; | ||||
| 
 | ||||
| /** | ||||
|  * Build app routes. | ||||
| @ -34,7 +35,16 @@ import { CoreArray } from '@singletons/array'; | ||||
|  * @return App routes. | ||||
|  */ | ||||
| function buildAppRoutes(injector: Injector): Routes { | ||||
|     return CoreArray.flatten(injector.get<Routes[]>(APP_ROUTES, [])); | ||||
|     const appRoutes = CoreArray.flatten(injector.get<Routes[]>(APP_ROUTES, [])); | ||||
| 
 | ||||
|     return appRoutes.map(route => { | ||||
|         route.canLoad = route.canLoad ?? []; | ||||
|         route.canActivate = route.canActivate ?? []; | ||||
|         route.canLoad.push(CoreRedirectGuard); | ||||
|         route.canActivate.push(CoreRedirectGuard); | ||||
| 
 | ||||
|         return route; | ||||
|     }); | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  | ||||
| @ -17,17 +17,16 @@ import { Observable } from 'rxjs'; | ||||
| import { AppComponent } from '@/app/app.component'; | ||||
| import { CoreApp } from '@services/app'; | ||||
| import { CoreEvents } from '@singletons/events'; | ||||
| import { CoreLangProvider } from '@services/lang'; | ||||
| import { CoreLang, CoreLangProvider } from '@services/lang'; | ||||
| import { Network, Platform, NgZone } from '@singletons'; | ||||
| 
 | ||||
| import { mock, mockSingleton, renderComponent, RenderConfig } from '@/testing/utils'; | ||||
| import { mockSingleton, renderComponent } from '@/testing/utils'; | ||||
| import { CoreNavigator, CoreNavigatorService } from '@services/navigator'; | ||||
| 
 | ||||
| describe('AppComponent', () => { | ||||
| 
 | ||||
|     let langProvider: CoreLangProvider; | ||||
|     let navigator: CoreNavigatorService; | ||||
|     let config: Partial<RenderConfig>; | ||||
| 
 | ||||
|     beforeEach(() => { | ||||
|         mockSingleton(CoreApp, { setStatusBarColor: jest.fn() }); | ||||
| @ -36,23 +35,18 @@ describe('AppComponent', () => { | ||||
|         mockSingleton(NgZone, { run: jest.fn() }); | ||||
| 
 | ||||
|         navigator = mockSingleton(CoreNavigator, ['navigate']); | ||||
|         langProvider = mock<CoreLangProvider>(['clearCustomStrings']); | ||||
|         config = { | ||||
|             providers: [ | ||||
|                 { provide: CoreLangProvider, useValue: langProvider }, | ||||
|             ], | ||||
|         }; | ||||
|         langProvider = mockSingleton(CoreLang, ['clearCustomStrings']); | ||||
|     }); | ||||
| 
 | ||||
|     it('should render', async () => { | ||||
|         const fixture = await renderComponent(AppComponent, config); | ||||
|         const fixture = await renderComponent(AppComponent); | ||||
| 
 | ||||
|         expect(fixture.debugElement.componentInstance).toBeTruthy(); | ||||
|         expect(fixture.nativeElement.querySelector('ion-router-outlet')).toBeTruthy(); | ||||
|     }); | ||||
| 
 | ||||
|     it('cleans up on logout', async () => { | ||||
|         const fixture = await renderComponent(AppComponent, config); | ||||
|         const fixture = await renderComponent(AppComponent); | ||||
| 
 | ||||
|         fixture.componentInstance.ngOnInit(); | ||||
|         CoreEvents.trigger(CoreEvents.LOGOUT); | ||||
| @ -61,6 +55,4 @@ describe('AppComponent', () => { | ||||
|         expect(navigator.navigate).toHaveBeenCalledWith('/login/sites', { reset: true }); | ||||
|     }); | ||||
| 
 | ||||
|     it.todo('shows loading while app isn\'t ready'); | ||||
| 
 | ||||
| }); | ||||
|  | ||||
| @ -12,10 +12,11 @@ | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Component, OnInit } from '@angular/core'; | ||||
| import { AfterViewInit, Component, OnInit, ViewChild } from '@angular/core'; | ||||
| import { IonRouterOutlet } from '@ionic/angular'; | ||||
| 
 | ||||
| import { CoreLangProvider } from '@services/lang'; | ||||
| import { CoreLoginHelperProvider } from '@features/login/services/login-helper'; | ||||
| import { CoreLang } from '@services/lang'; | ||||
| import { CoreLoginHelper } from '@features/login/services/login-helper'; | ||||
| import { | ||||
|     CoreEvents, | ||||
|     CoreEventSessionExpiredData, | ||||
| @ -23,23 +24,20 @@ import { | ||||
|     CoreEventSiteData, | ||||
|     CoreEventSiteUpdatedData, | ||||
| } from '@singletons/events'; | ||||
| import { Network, NgZone, Platform } from '@singletons'; | ||||
| import { Network, NgZone, Platform, SplashScreen } from '@singletons'; | ||||
| import { CoreApp } from '@services/app'; | ||||
| import { CoreSites } from '@services/sites'; | ||||
| import { CoreNavigator } from '@services/navigator'; | ||||
| import { CoreSubscriptions } from '@singletons/subscriptions'; | ||||
| 
 | ||||
| @Component({ | ||||
|     selector: 'app-root', | ||||
|     templateUrl: 'app.component.html', | ||||
|     styleUrls: ['app.component.scss'], | ||||
| }) | ||||
| export class AppComponent implements OnInit { | ||||
| export class AppComponent implements OnInit, AfterViewInit { | ||||
| 
 | ||||
|     constructor( | ||||
|         protected langProvider: CoreLangProvider, | ||||
|         protected loginHelper: CoreLoginHelperProvider, | ||||
|     ) { | ||||
|     } | ||||
|     @ViewChild(IonRouterOutlet) outlet?: IonRouterOutlet; | ||||
| 
 | ||||
|     /** | ||||
|      * Component being initialized. | ||||
| @ -58,7 +56,7 @@ export class AppComponent implements OnInit { | ||||
|             CoreNavigator.instance.navigate('/login/sites', { reset: true }); | ||||
| 
 | ||||
|             // Unload lang custom strings.
 | ||||
|             this.langProvider.clearCustomStrings(); | ||||
|             CoreLang.instance.clearCustomStrings(); | ||||
| 
 | ||||
|             // Remove version classes from body.
 | ||||
|             this.removeVersionClass(); | ||||
| @ -66,20 +64,20 @@ export class AppComponent implements OnInit { | ||||
| 
 | ||||
|         // Listen for session expired events.
 | ||||
|         CoreEvents.on(CoreEvents.SESSION_EXPIRED, (data: CoreEventSessionExpiredData) => { | ||||
|             this.loginHelper.sessionExpired(data); | ||||
|             CoreLoginHelper.instance.sessionExpired(data); | ||||
|         }); | ||||
| 
 | ||||
|         // Listen for passwordchange and usernotfullysetup events to open InAppBrowser.
 | ||||
|         CoreEvents.on(CoreEvents.PASSWORD_CHANGE_FORCED, (data: CoreEventSiteData) => { | ||||
|             this.loginHelper.passwordChangeForced(data.siteId!); | ||||
|             CoreLoginHelper.instance.passwordChangeForced(data.siteId!); | ||||
|         }); | ||||
|         CoreEvents.on(CoreEvents.USER_NOT_FULLY_SETUP, (data: CoreEventSiteData) => { | ||||
|             this.loginHelper.openInAppForEdit(data.siteId!, '/user/edit.php', 'core.usernotfullysetup'); | ||||
|             CoreLoginHelper.instance.openInAppForEdit(data.siteId!, '/user/edit.php', 'core.usernotfullysetup'); | ||||
|         }); | ||||
| 
 | ||||
|         // Listen for sitepolicynotagreed event to accept the site policy.
 | ||||
|         CoreEvents.on(CoreEvents.SITE_POLICY_NOT_AGREED, (data: CoreEventSiteData) => { | ||||
|             this.loginHelper.sitePolicyNotAgreed(data.siteId); | ||||
|             CoreLoginHelper.instance.sitePolicyNotAgreed(data.siteId); | ||||
|         }); | ||||
| 
 | ||||
|         CoreEvents.on(CoreEvents.LOGIN, async (data: CoreEventSiteData) => { | ||||
| @ -119,6 +117,17 @@ export class AppComponent implements OnInit { | ||||
|         this.onPlatformReady(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     ngAfterViewInit(): void { | ||||
|         if (!this.outlet) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         CoreSubscriptions.once(this.outlet.activateEvents, () => SplashScreen.instance.hide()); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Async init function on platform ready. | ||||
|      */ | ||||
| @ -155,8 +164,9 @@ export class AppComponent implements OnInit { | ||||
|      */ | ||||
|     protected loadCustomStrings(): void { | ||||
|         const currentSite = CoreSites.instance.getCurrentSite(); | ||||
| 
 | ||||
|         if (currentSite) { | ||||
|             this.langProvider.loadCustomStringsFromSite(currentSite); | ||||
|             CoreLang.instance.loadCustomStringsFromSite(currentSite); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | ||||
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 55 KiB | 
| @ -38,6 +38,7 @@ import { CoreLinkDirective } from './link'; | ||||
| import { CoreFilter, CoreFilterFilter, CoreFilterFormatTextOptions } from '@features/filter/services/filter'; | ||||
| import { CoreFilterDelegate } from '@features/filter/services/filter-delegate'; | ||||
| import { CoreFilterHelper } from '@features/filter/services/filter-helper'; | ||||
| import { CoreSubscriptions } from '@singletons/subscriptions'; | ||||
| 
 | ||||
| /** | ||||
|  * Directive to format text rendered. It renders the HTML and treats all links and media, using CoreLinkDirective | ||||
| @ -567,12 +568,7 @@ export class CoreFormatTextDirective implements OnChanges { | ||||
|                     return Promise.resolve(); | ||||
|                 } | ||||
| 
 | ||||
|                 return new Promise((resolve): void => { | ||||
|                     const subscription = externalImage.onLoad.subscribe(() => { | ||||
|                         subscription.unsubscribe(); | ||||
|                         resolve(); | ||||
|                     }); | ||||
|                 }); | ||||
|                 return new Promise(resolve => CoreSubscriptions.once(externalImage.onLoad, resolve)); | ||||
|             })); | ||||
| 
 | ||||
|             // Automatically reject the promise after 5 seconds to prevent blocking the user forever.
 | ||||
|  | ||||
							
								
								
									
										59
									
								
								src/core/features/login/guards/has-sites.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								src/core/features/login/guards/has-sites.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,59 @@ | ||||
| // (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 { CanActivate, CanLoad, UrlTree } from '@angular/router'; | ||||
| 
 | ||||
| import { CoreSites } from '@services/sites'; | ||||
| import { CoreUtils } from '@services/utils/utils'; | ||||
| import { Router } from '@singletons'; | ||||
| 
 | ||||
| import { CoreLoginHelper } from '../services/login-helper'; | ||||
| 
 | ||||
| @Injectable({ providedIn: 'root' }) | ||||
| export class CoreLoginHasSitesGuard implements CanActivate, CanLoad { | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     canActivate(): Promise<true | UrlTree> { | ||||
|         return this.guard(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     canLoad(): Promise<true | UrlTree> { | ||||
|         return this.guard(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if the user has any sites stored. | ||||
|      */ | ||||
|     private async guard(): Promise<true | UrlTree> { | ||||
|         const sites = await CoreUtils.instance.ignoreErrors(CoreSites.instance.getSites(), []); | ||||
| 
 | ||||
|         if (sites.length > 0) { | ||||
|             return true; | ||||
|         } | ||||
| 
 | ||||
|         const [path, params] = CoreLoginHelper.instance.getAddSiteRouteInfo(); | ||||
|         const route = Router.instance.parseUrl(path); | ||||
| 
 | ||||
|         route.queryParams = params; | ||||
| 
 | ||||
|         return route; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| @ -21,16 +21,13 @@ import { TranslateModule } from '@ngx-translate/core'; | ||||
| import { CoreSharedModule } from '@/core/shared.module'; | ||||
| import { CoreLoginSiteHelpComponent } from './components/site-help/site-help'; | ||||
| import { CoreLoginSiteOnboardingComponent } from './components/site-onboarding/site-onboarding'; | ||||
| import { CoreLoginHasSitesGuard } from './guards/has-sites'; | ||||
| 
 | ||||
| const routes: Routes = [ | ||||
|     { | ||||
|         path: '', | ||||
|         redirectTo: 'init', | ||||
|         pathMatch: 'full', | ||||
|     }, | ||||
|     { | ||||
|         path: 'init', | ||||
|         loadChildren: () => import('./pages/init/init.module').then( m => m.CoreLoginInitPageModule), | ||||
|         redirectTo: 'sites', | ||||
|     }, | ||||
|     { | ||||
|         path: 'site', | ||||
| @ -43,6 +40,8 @@ const routes: Routes = [ | ||||
|     { | ||||
|         path: 'sites', | ||||
|         loadChildren: () => import('./pages/sites/sites.module').then( m => m.CoreLoginSitesPageModule), | ||||
|         canLoad: [CoreLoginHasSitesGuard], | ||||
|         canActivate: [CoreLoginHasSitesGuard], | ||||
|     }, | ||||
|     { | ||||
|         path: 'forgottenpassword', | ||||
|  | ||||
| @ -1,7 +0,0 @@ | ||||
| <ion-content fullscreen="true" scrollY="false"> | ||||
|     <div class="core-bglogo" slot="fixed"> | ||||
|         <div class="core-center-spinner"> | ||||
|             <ion-spinner></ion-spinner> | ||||
|         </div> | ||||
|     </div> | ||||
| </ion-content> | ||||
| @ -1,25 +0,0 @@ | ||||
| ion-content::part(background) { | ||||
|     --background: var(--core-splash-screen-background, #ffffff); | ||||
| 
 | ||||
|     background-image: url("~@/assets/img/splash.png"); | ||||
|     background-repeat: no-repeat; | ||||
|     background-size: 100%; | ||||
|     background-size: var(--core-splash-bgsize, 100vmax); | ||||
|     background-position: center; | ||||
| } | ||||
| 
 | ||||
| .core-bglogo { | ||||
|     display: table; | ||||
|     width: 100%; | ||||
|     height: 100%; | ||||
| 
 | ||||
|     .core-center-spinner { | ||||
|         display: table-cell; | ||||
|         vertical-align: middle; | ||||
|         text-align: center; | ||||
|     } | ||||
| 
 | ||||
|     ion-spinner { | ||||
|         --color: var(--core-splash-spinner-color, var(--core-color)); | ||||
|     } | ||||
| } | ||||
| @ -1,125 +0,0 @@ | ||||
| // (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 { Component, OnInit } from '@angular/core'; | ||||
| 
 | ||||
| import { CoreApp, CoreRedirectData } from '@services/app'; | ||||
| import { ApplicationInit, SplashScreen } from '@singletons'; | ||||
| import { CoreConstants } from '@/core/constants'; | ||||
| import { CoreSites } from '@services/sites'; | ||||
| import { CoreLoginHelper } from '@features/login/services/login-helper'; | ||||
| import { CoreNavigator } from '@services/navigator'; | ||||
| 
 | ||||
| /** | ||||
|  * Page that displays a "splash screen" while the app is being initialized. | ||||
|  */ | ||||
| @Component({ | ||||
|     selector: 'page-core-login-init', | ||||
|     templateUrl: 'init.html', | ||||
|     styleUrls: ['init.scss'], | ||||
| }) | ||||
| export class CoreLoginInitPage implements OnInit { | ||||
| 
 | ||||
|     // @todo this page should be removed in favor of native splash
 | ||||
|     // or a splash component rendered in the root app component
 | ||||
| 
 | ||||
|     /** | ||||
|      * Initialize the component. | ||||
|      */ | ||||
|     async ngOnInit(): Promise<void> { | ||||
|         // Wait for the app to be ready.
 | ||||
|         await ApplicationInit.instance.donePromise; | ||||
| 
 | ||||
|         // Check if there was a pending redirect.
 | ||||
|         const redirectData = CoreApp.instance.getRedirect(); | ||||
| 
 | ||||
|         if (redirectData.siteId) { | ||||
|             await this.handleRedirect(redirectData); | ||||
|         } else { | ||||
|             await this.loadPage(); | ||||
|         } | ||||
| 
 | ||||
|         // If we hide the splash screen now, the init view is still seen for an instant. Wait a bit to make sure it isn't seen.
 | ||||
|         setTimeout(() => { | ||||
|             SplashScreen.instance.hide(); | ||||
|         }, 100); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Treat redirect data. | ||||
|      * | ||||
|      * @param redirectData Redirect data. | ||||
|      */ | ||||
|     protected async handleRedirect(redirectData: CoreRedirectData): Promise<void> { | ||||
|         // Unset redirect data.
 | ||||
|         CoreApp.instance.storeRedirect('', '', {}); | ||||
| 
 | ||||
|         // Only accept the redirect if it was stored less than 20 seconds ago.
 | ||||
|         if (redirectData.timemodified && Date.now() - redirectData.timemodified < 20000) { | ||||
|             if (redirectData.siteId != CoreConstants.NO_SITE_ID) { | ||||
|                 // The redirect is pointing to a site, load it.
 | ||||
|                 try { | ||||
|                     const loggedIn = await CoreSites.instance.loadSite( | ||||
|                         redirectData.siteId!, | ||||
|                         redirectData.page, | ||||
|                         redirectData.params, | ||||
|                     ); | ||||
| 
 | ||||
|                     if (!loggedIn) { | ||||
|                         return; | ||||
|                     } | ||||
| 
 | ||||
|                     await CoreNavigator.instance.navigateToSiteHome({ | ||||
|                         params: { | ||||
|                             redirectPath: redirectData.page, | ||||
|                             redirectParams: redirectData.params, | ||||
|                         }, | ||||
|                     }); | ||||
| 
 | ||||
|                     return; | ||||
|                 } catch (error) { | ||||
|                     // Site doesn't exist.
 | ||||
|                     return this.loadPage(); | ||||
|                 } | ||||
|             } else if (redirectData.page) { | ||||
|                 // No site to load, open the page.
 | ||||
|                 // @todo return CoreNavigator.instance.goToNoSitePage(redirectData.page, redirectData.params);
 | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return this.loadPage(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Load the right page. | ||||
|      * | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     protected async loadPage(): Promise<void> { | ||||
|         if (CoreSites.instance.isLoggedIn()) { | ||||
|             if (CoreLoginHelper.instance.isSiteLoggedOut()) { | ||||
|                 await CoreSites.instance.logout(); | ||||
| 
 | ||||
|                 return this.loadPage(); | ||||
|             } | ||||
| 
 | ||||
|             await CoreNavigator.instance.navigateToSiteHome(); | ||||
| 
 | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         await CoreNavigator.instance.navigate('/login/sites', { reset: true }); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| @ -45,13 +45,7 @@ export class CoreLoginSitesPage implements OnInit { | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async ngOnInit(): Promise<void> { | ||||
|         const sites = await CoreUtils.instance.ignoreErrors(CoreSites.instance.getSortedSites()); | ||||
| 
 | ||||
|         if (!sites || sites.length == 0) { | ||||
|             CoreLoginHelper.instance.goToAddSite(true); | ||||
| 
 | ||||
|             return; | ||||
|         } | ||||
|         const sites = await CoreUtils.instance.ignoreErrors(CoreSites.instance.getSortedSites(), [] as CoreSiteBasicInfo[]); | ||||
| 
 | ||||
|         // Remove protocol from the url to show more url text.
 | ||||
|         this.sites = sites.map((site) => { | ||||
|  | ||||
| @ -34,6 +34,7 @@ import { makeSingleton, Translate } from '@singletons'; | ||||
| import { CoreLogger } from '@singletons/logger'; | ||||
| import { CoreUrl } from '@singletons/url'; | ||||
| import { CoreNavigator } from '@services/navigator'; | ||||
| import { CoreObject } from '@singletons/object'; | ||||
| 
 | ||||
| /** | ||||
|  * Helper provider that provides some common features regarding authentication. | ||||
| @ -408,22 +409,27 @@ export class CoreLoginHelperProvider { | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async goToAddSite(setRoot?: boolean, showKeyboard?: boolean): Promise<void> { | ||||
|         let pageRoute: string; | ||||
|         let params: Params; | ||||
|         const [path, params] = this.getAddSiteRouteInfo(showKeyboard); | ||||
| 
 | ||||
|         await CoreNavigator.instance.navigate(path, { params, reset: setRoot }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get path and params to visit the route to add site. | ||||
|      * | ||||
|      * @param showKeyboard Whether to show keyboard in the new page. Only if no fixed URL set. | ||||
|      * @return Path and params. | ||||
|      */ | ||||
|     getAddSiteRouteInfo(showKeyboard?: boolean): [string, Params] { | ||||
|         if (this.isFixedUrlSet()) { | ||||
|             // Fixed URL is set, go to credentials page.
 | ||||
|             const fixedSites = this.getFixedSites(); | ||||
|             const url = typeof fixedSites == 'string' ? fixedSites : fixedSites[0].url; | ||||
| 
 | ||||
|             pageRoute = '/login/credentials'; | ||||
|             params = { siteUrl: url }; | ||||
|         } else { | ||||
|             pageRoute = '/login/site'; | ||||
|             params = { showKeyboard: showKeyboard }; | ||||
|             return ['/login/credentials', { siteUrl: url }]; | ||||
|         } | ||||
| 
 | ||||
|         await CoreNavigator.instance.navigate(pageRoute, { params, reset: setRoot }); | ||||
|         return ['/login/site', CoreObject.withoutEmpty({ showKeyboard: showKeyboard })]; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -1,52 +0,0 @@ | ||||
| // (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 { CoreApp } from '@services/app'; | ||||
| import { CoreLoginInitPage } from '@features/login/pages/init/init'; | ||||
| import { CoreSites } from '@services/sites'; | ||||
| import { ApplicationInit, SplashScreen } from '@singletons'; | ||||
| 
 | ||||
| import { mockSingleton, renderComponent } from '@/testing/utils'; | ||||
| import { CoreNavigator, CoreNavigatorService } from '@services/navigator'; | ||||
| 
 | ||||
| describe('CoreLoginInitPage', () => { | ||||
| 
 | ||||
|     let navigator: CoreNavigatorService; | ||||
| 
 | ||||
|     beforeEach(() => { | ||||
|         mockSingleton(CoreApp, { getRedirect: () => ({}) }); | ||||
|         mockSingleton(ApplicationInit, { donePromise: Promise.resolve() }); | ||||
|         mockSingleton(CoreSites, { isLoggedIn: () => false }); | ||||
|         mockSingleton(SplashScreen, ['hide']); | ||||
| 
 | ||||
|         navigator = mockSingleton(CoreNavigator, ['navigate']); | ||||
|     }); | ||||
| 
 | ||||
|     it('should render', async () => { | ||||
|         const fixture = await renderComponent(CoreLoginInitPage, {}); | ||||
| 
 | ||||
|         expect(fixture.debugElement.componentInstance).toBeTruthy(); | ||||
|         expect(fixture.nativeElement.querySelector('ion-spinner')).toBeTruthy(); | ||||
|     }); | ||||
| 
 | ||||
|     it('navigates to sites page after loading', async () => { | ||||
|         const fixture = await renderComponent(CoreLoginInitPage, {}); | ||||
| 
 | ||||
|         fixture.componentInstance.ngOnInit(); | ||||
|         await ApplicationInit.instance.donePromise; | ||||
| 
 | ||||
|         expect(navigator.navigate).toHaveBeenCalledWith('/login/sites', { reset: true }); | ||||
|     }); | ||||
| 
 | ||||
| }); | ||||
| @ -13,28 +13,44 @@ | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { Router, CanLoad, CanActivate, UrlTree } from '@angular/router'; | ||||
| import { CanLoad, CanActivate, UrlTree } from '@angular/router'; | ||||
| import { CoreLoginHelper } from '@features/login/services/login-helper'; | ||||
| 
 | ||||
| import { CoreSites } from '@services/sites'; | ||||
| import { ApplicationInit } from '@singletons'; | ||||
| import { Router } from '@singletons'; | ||||
| 
 | ||||
| @Injectable({ providedIn: 'root' }) | ||||
| export class AuthGuard implements CanLoad, CanActivate { | ||||
| 
 | ||||
|     constructor(private router: Router) {} | ||||
| export class CoreMainMenuAuthGuard implements CanLoad, CanActivate { | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     canActivate(): Promise<true | UrlTree> { | ||||
|         return this.guard(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     canLoad(): Promise<true | UrlTree> { | ||||
|         return this.guard(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if the current user should be redirected to the authentication page. | ||||
|      */ | ||||
|     private async guard(): Promise<true | UrlTree> { | ||||
|         await ApplicationInit.instance.donePromise; | ||||
|         if (!CoreSites.instance.isLoggedIn()) { | ||||
|             return Router.instance.parseUrl('/login'); | ||||
|         } | ||||
| 
 | ||||
|         return CoreSites.instance.isLoggedIn() || this.router.parseUrl('/login'); | ||||
|         if (CoreLoginHelper.instance.isSiteLoggedOut()) { | ||||
|             await CoreSites.instance.logout(); | ||||
| 
 | ||||
|             return Router.instance.parseUrl('/login'); | ||||
|         } | ||||
| 
 | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| @ -14,7 +14,7 @@ | ||||
| 
 | ||||
| import { APP_INITIALIZER, NgModule } from '@angular/core'; | ||||
| import { Routes } from '@angular/router'; | ||||
| import { AuthGuard } from '@guards/auth'; | ||||
| import { CoreMainMenuAuthGuard } from '@features/mainmenu/guards/auth'; | ||||
| 
 | ||||
| import { AppRoutingModule } from '@/app/app-routing.module'; | ||||
| 
 | ||||
| @ -30,8 +30,8 @@ const appRoutes: Routes = [ | ||||
|     { | ||||
|         path: 'main', | ||||
|         loadChildren: () => import('./mainmenu-lazy.module').then(m => m.CoreMainMenuLazyModule), | ||||
|         canActivate: [AuthGuard], | ||||
|         canLoad: [AuthGuard], | ||||
|         canActivate: [CoreMainMenuAuthGuard], | ||||
|         canLoad: [CoreMainMenuAuthGuard], | ||||
|     }, | ||||
| ]; | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										92
									
								
								src/core/guards/redirect.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								src/core/guards/redirect.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,92 @@ | ||||
| // (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 { CanActivate, CanLoad, UrlTree } from '@angular/router'; | ||||
| import { CoreApp } from '@services/app'; | ||||
| import { CoreSites } from '@services/sites'; | ||||
| import { Router } from '@singletons'; | ||||
| import { CoreObject } from '@singletons/object'; | ||||
| import { CoreConstants } from '../constants'; | ||||
| 
 | ||||
| @Injectable({ providedIn: 'root' }) | ||||
| export class CoreRedirectGuard implements CanLoad, CanActivate { | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     canLoad(): Promise<true | UrlTree> { | ||||
|         return this.guard(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     canActivate(): Promise<true | UrlTree> { | ||||
|         return this.guard(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if there is a pending redirect and trigger it. | ||||
|      */ | ||||
|     private async guard(): Promise<true | UrlTree> { | ||||
|         const redirect = CoreApp.instance.getRedirect(); | ||||
| 
 | ||||
|         if (!redirect) { | ||||
|             return true; | ||||
|         } | ||||
| 
 | ||||
|         try { | ||||
|             // Only accept the redirect if it was stored less than 20 seconds ago.
 | ||||
|             if (!redirect.timemodified || Date.now() - redirect.timemodified < 20000) { | ||||
|                 return true; | ||||
|             } | ||||
| 
 | ||||
|             // Redirect to site path.
 | ||||
|             if (redirect.siteId && redirect.siteId !== CoreConstants.NO_SITE_ID) { | ||||
|                 const loggedIn = await CoreSites.instance.loadSite( | ||||
|                     redirect.siteId, | ||||
|                     redirect.page, | ||||
|                     redirect.params, | ||||
|                 ); | ||||
|                 const route = Router.instance.parseUrl('/main'); | ||||
| 
 | ||||
|                 route.queryParams = CoreObject.withoutEmpty({ | ||||
|                     redirectPath: redirect.page, | ||||
|                     redirectParams: redirect.params, | ||||
|                 }); | ||||
| 
 | ||||
|                 return loggedIn ? route : true; | ||||
|             } | ||||
| 
 | ||||
|             // Abort redirect.
 | ||||
|             if (!redirect.page) { | ||||
|                 return true; | ||||
|             } | ||||
| 
 | ||||
|             // Redirect to non-site path.
 | ||||
|             const route = Router.instance.parseUrl(redirect.page); | ||||
| 
 | ||||
|             route.queryParams = CoreObject.withoutEmpty({ | ||||
|                 redirectPath: redirect.page, | ||||
|                 redirectParams: redirect.params, | ||||
|             }); | ||||
| 
 | ||||
|             return route; | ||||
|         } finally { | ||||
|             CoreApp.instance.forgetRedirect(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| @ -12,27 +12,8 @@ | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { NgModule } from '@angular/core'; | ||||
| import { RouterModule, Routes } from '@angular/router'; | ||||
| import { IonicModule } from '@ionic/angular'; | ||||
| import { CoreApp } from '@services/app'; | ||||
| 
 | ||||
| import { CoreLoginInitPage } from './init'; | ||||
| 
 | ||||
| const routes: Routes = [ | ||||
|     { | ||||
|         path: '', | ||||
|         component: CoreLoginInitPage, | ||||
|     }, | ||||
| ]; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     imports: [ | ||||
|         RouterModule.forChild(routes), | ||||
|         IonicModule, | ||||
|     ], | ||||
|     declarations: [ | ||||
|         CoreLoginInitPage, | ||||
|     ], | ||||
|     exports: [RouterModule], | ||||
| }) | ||||
| export class CoreLoginInitPageModule {} | ||||
| export default function(): void { | ||||
|     CoreApp.instance.consumeStorageRedirect(); | ||||
| } | ||||
| @ -25,6 +25,7 @@ import { makeSingleton, Keyboard, Network, StatusBar, Platform, Device } from '@ | ||||
| import { CoreLogger } from '@singletons/logger'; | ||||
| import { CoreColors } from '@singletons/colors'; | ||||
| import { DBNAME, SCHEMA_VERSIONS_TABLE_NAME, SCHEMA_VERSIONS_TABLE_SCHEMA, SchemaVersionsDBEntry } from '@services/database/app'; | ||||
| import { CoreObject } from '@singletons/object'; | ||||
| 
 | ||||
| /** | ||||
|  * Object responsible of managing schema versions. | ||||
| @ -58,6 +59,7 @@ export class CoreAppProvider { | ||||
|     protected keyboardClosing = false; | ||||
|     protected backActions: {callback: () => boolean; priority: number}[] = []; | ||||
|     protected forceOffline = false; | ||||
|     protected redirect?: CoreRedirectData; | ||||
| 
 | ||||
|     // Variables for DB.
 | ||||
|     protected schemaVersionsManager: Promise<SchemaVersionsManager>; | ||||
| @ -516,32 +518,50 @@ export class CoreAppProvider { | ||||
|         await deferred.promise; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Read redirect data from local storage and clear it if it existed. | ||||
|      */ | ||||
|     consumeStorageRedirect(): void { | ||||
|         if (!localStorage?.getItem) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         try { | ||||
|             // Read data from storage.
 | ||||
|             const jsonData = localStorage.getItem('CoreRedirect'); | ||||
| 
 | ||||
|             if (!jsonData) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             // Clear storage.
 | ||||
|             localStorage.removeItem('CoreRedirect'); | ||||
| 
 | ||||
|             // Remember redirect data.
 | ||||
|             const data: CoreRedirectData = JSON.parse(jsonData); | ||||
| 
 | ||||
|             if (!CoreObject.isEmpty(data)) { | ||||
|                 this.redirect = data; | ||||
|             } | ||||
|         } catch (error) { | ||||
|             this.logger.error('Error loading redirect data:', error); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Forget redirect data. | ||||
|      */ | ||||
|     forgetRedirect(): void { | ||||
|         delete this.redirect; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Retrieve redirect data. | ||||
|      * | ||||
|      * @return Object with siteid, state, params and timemodified. | ||||
|      */ | ||||
|     getRedirect(): CoreRedirectData { | ||||
|         if (localStorage?.getItem) { | ||||
|             try { | ||||
|                 const paramsJson = localStorage.getItem('CoreRedirectParams'); | ||||
|                 const data: CoreRedirectData = { | ||||
|                     siteId: localStorage.getItem('CoreRedirectSiteId') || undefined, | ||||
|                     page: localStorage.getItem('CoreRedirectState')  || undefined, | ||||
|                     timemodified: parseInt(localStorage.getItem('CoreRedirectTime') || '0', 10), | ||||
|                 }; | ||||
| 
 | ||||
|                 if (paramsJson) { | ||||
|                     data.params = JSON.parse(paramsJson); | ||||
|                 } | ||||
| 
 | ||||
|                 return data; | ||||
|             } catch (ex) { | ||||
|                 this.logger.error('Error loading redirect data:', ex); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return {}; | ||||
|     getRedirect(): CoreRedirectData | null { | ||||
|         return this.redirect || null; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -552,15 +572,17 @@ export class CoreAppProvider { | ||||
|      * @param params Page params. | ||||
|      */ | ||||
|     storeRedirect(siteId: string, page: string, params: Params): void { | ||||
|         if (localStorage && localStorage.setItem) { | ||||
|             try { | ||||
|                 localStorage.setItem('CoreRedirectSiteId', siteId); | ||||
|                 localStorage.setItem('CoreRedirectState', page); | ||||
|                 localStorage.setItem('CoreRedirectParams', JSON.stringify(params)); | ||||
|                 localStorage.setItem('CoreRedirectTime', String(Date.now())); | ||||
|             } catch (ex) { | ||||
|                 // Ignore errors.
 | ||||
|             } | ||||
|         try { | ||||
|             const redirect: CoreRedirectData = { | ||||
|                 siteId, | ||||
|                 page, | ||||
|                 params, | ||||
|                 timemodified: Date.now(), | ||||
|             }; | ||||
| 
 | ||||
|             localStorage.setItem('CoreRedirect', JSON.stringify(redirect)); | ||||
|         } catch (ex) { | ||||
|             // Ignore errors.
 | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -18,6 +18,7 @@ import { CoreConstants } from '@/core/constants'; | ||||
| import { LangChangeEvent } from '@ngx-translate/core'; | ||||
| import { CoreAppProvider } from '@services/app'; | ||||
| import { CoreConfig } from '@services/config'; | ||||
| import { CoreSubscriptions } from '@singletons/subscriptions'; | ||||
| import { makeSingleton, Translate, Platform } from '@singletons'; | ||||
| 
 | ||||
| import * as moment from 'moment'; | ||||
| @ -128,44 +129,25 @@ export class CoreLangProvider { | ||||
| 
 | ||||
|         // Change the language, resolving the promise when we receive the first value.
 | ||||
|         promises.push(new Promise((resolve, reject) => { | ||||
|             const subscription = Translate.instance.use(language).subscribe((data) => { | ||||
|             CoreSubscriptions.once(Translate.instance.use(language), data => { | ||||
|                 // It's a language override, load the original one first.
 | ||||
|                 const fallbackLang = Translate.instance.instant('core.parentlanguage'); | ||||
| 
 | ||||
|                 if (fallbackLang != '' && fallbackLang != 'core.parentlanguage' && fallbackLang != language) { | ||||
|                     const fallbackSubs = Translate.instance.use(fallbackLang).subscribe((fallbackData) => { | ||||
|                         data = Object.assign(fallbackData, data); | ||||
|                         resolve(data); | ||||
|                     CoreSubscriptions.once( | ||||
|                         Translate.instance.use(fallbackLang), | ||||
|                         fallbackData => { | ||||
|                             data = Object.assign(fallbackData, data); | ||||
| 
 | ||||
|                         // Data received, unsubscribe. Use a timeout because we can receive a value immediately.
 | ||||
|                         setTimeout(() => { | ||||
|                             fallbackSubs.unsubscribe(); | ||||
|                         }); | ||||
|                     }, () => { | ||||
|                             resolve(data); | ||||
|                         }, | ||||
|                         // Resolve with the original language.
 | ||||
|                         resolve(data); | ||||
| 
 | ||||
|                         // Error received, unsubscribe. Use a timeout because we can receive a value immediately.
 | ||||
|                         setTimeout(() => { | ||||
|                             fallbackSubs.unsubscribe(); | ||||
|                         }); | ||||
|                     }); | ||||
|                         () => resolve(data), | ||||
|                     ); | ||||
|                 } else { | ||||
|                     resolve(data); | ||||
|                 } | ||||
| 
 | ||||
|                 // Data received, unsubscribe. Use a timeout because we can receive a value immediately.
 | ||||
|                 setTimeout(() => { | ||||
|                     subscription.unsubscribe(); | ||||
|                 }); | ||||
|             }, (error) => { | ||||
|                 reject(error); | ||||
| 
 | ||||
|                 // Error received, unsubscribe. Use a timeout because we can receive a value immediately.
 | ||||
|                 setTimeout(() => { | ||||
|                     subscription.unsubscribe(); | ||||
|                 }); | ||||
|             }); | ||||
|             }, reject); | ||||
|         })); | ||||
| 
 | ||||
|         // Change the config.
 | ||||
|  | ||||
							
								
								
									
										52
									
								
								src/core/singletons/subscriptions.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								src/core/singletons/subscriptions.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,52 @@ | ||||
| // (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 { EventEmitter } from '@angular/core'; | ||||
| import { Observable } from 'rxjs'; | ||||
| 
 | ||||
| /** | ||||
|  * Subscribable object. | ||||
|  */ | ||||
| type Subscribable<T> = EventEmitter<T> | Observable<T>; | ||||
| 
 | ||||
| /** | ||||
|  * Singleton with helpers to work with subscriptions. | ||||
|  */ | ||||
| export class CoreSubscriptions { | ||||
| 
 | ||||
|     /** | ||||
|      * Listen once to a subscribable object. | ||||
|      * | ||||
|      * @param subscribable Subscribable to listen to. | ||||
|      * @param onSuccess Callback to run when the subscription is updated. | ||||
|      * @param onError Callback to run when the an error happens. | ||||
|      */ | ||||
|     static once<T>(subscribable: Subscribable<T>, onSuccess: (value: T) => unknown, onError?: (error: unknown) => unknown): void { | ||||
|         const subscription = subscribable.subscribe( | ||||
|             value => { | ||||
|                 // Unsubscribe using a timeout because we can receive a value immediately.
 | ||||
|                 setTimeout(() => subscription.unsubscribe(), 0); | ||||
| 
 | ||||
|                 onSuccess(value); | ||||
|             }, | ||||
|             error => { | ||||
|                 // Unsubscribe using a timeout because we can receive a value immediately.
 | ||||
|                 setTimeout(() => subscription.unsubscribe(), 0); | ||||
| 
 | ||||
|                 onError?.call(error); | ||||
|             }, | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user