MOBILE-3689 init: Replace /login/init with guards

main
Noel De Martin 2021-02-02 18:41:58 +01:00
parent 033860d18b
commit 2a5e29b1c3
20 changed files with 359 additions and 357 deletions

View File

@ -26,6 +26,7 @@ import {
} from '@angular/router'; } from '@angular/router';
import { CoreArray } from '@singletons/array'; import { CoreArray } from '@singletons/array';
import { CoreRedirectGuard } from '@guards/redirect';
/** /**
* Build app routes. * Build app routes.
@ -34,7 +35,16 @@ import { CoreArray } from '@singletons/array';
* @return App routes. * @return App routes.
*/ */
function buildAppRoutes(injector: Injector): 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;
});
} }
/** /**

View File

@ -17,17 +17,16 @@ import { Observable } from 'rxjs';
import { AppComponent } from '@/app/app.component'; import { AppComponent } from '@/app/app.component';
import { CoreApp } from '@services/app'; import { CoreApp } from '@services/app';
import { CoreEvents } from '@singletons/events'; import { CoreEvents } from '@singletons/events';
import { CoreLangProvider } from '@services/lang'; import { CoreLang, CoreLangProvider } from '@services/lang';
import { Network, Platform, NgZone } from '@singletons'; 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'; import { CoreNavigator, CoreNavigatorService } from '@services/navigator';
describe('AppComponent', () => { describe('AppComponent', () => {
let langProvider: CoreLangProvider; let langProvider: CoreLangProvider;
let navigator: CoreNavigatorService; let navigator: CoreNavigatorService;
let config: Partial<RenderConfig>;
beforeEach(() => { beforeEach(() => {
mockSingleton(CoreApp, { setStatusBarColor: jest.fn() }); mockSingleton(CoreApp, { setStatusBarColor: jest.fn() });
@ -36,23 +35,18 @@ describe('AppComponent', () => {
mockSingleton(NgZone, { run: jest.fn() }); mockSingleton(NgZone, { run: jest.fn() });
navigator = mockSingleton(CoreNavigator, ['navigate']); navigator = mockSingleton(CoreNavigator, ['navigate']);
langProvider = mock<CoreLangProvider>(['clearCustomStrings']); langProvider = mockSingleton(CoreLang, ['clearCustomStrings']);
config = {
providers: [
{ provide: CoreLangProvider, useValue: langProvider },
],
};
}); });
it('should render', async () => { it('should render', async () => {
const fixture = await renderComponent(AppComponent, config); const fixture = await renderComponent(AppComponent);
expect(fixture.debugElement.componentInstance).toBeTruthy(); expect(fixture.debugElement.componentInstance).toBeTruthy();
expect(fixture.nativeElement.querySelector('ion-router-outlet')).toBeTruthy(); expect(fixture.nativeElement.querySelector('ion-router-outlet')).toBeTruthy();
}); });
it('cleans up on logout', async () => { it('cleans up on logout', async () => {
const fixture = await renderComponent(AppComponent, config); const fixture = await renderComponent(AppComponent);
fixture.componentInstance.ngOnInit(); fixture.componentInstance.ngOnInit();
CoreEvents.trigger(CoreEvents.LOGOUT); CoreEvents.trigger(CoreEvents.LOGOUT);
@ -61,6 +55,4 @@ describe('AppComponent', () => {
expect(navigator.navigate).toHaveBeenCalledWith('/login/sites', { reset: true }); expect(navigator.navigate).toHaveBeenCalledWith('/login/sites', { reset: true });
}); });
it.todo('shows loading while app isn\'t ready');
}); });

View File

@ -12,10 +12,11 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // 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 { CoreLang } from '@services/lang';
import { CoreLoginHelperProvider } from '@features/login/services/login-helper'; import { CoreLoginHelper } from '@features/login/services/login-helper';
import { import {
CoreEvents, CoreEvents,
CoreEventSessionExpiredData, CoreEventSessionExpiredData,
@ -23,23 +24,20 @@ import {
CoreEventSiteData, CoreEventSiteData,
CoreEventSiteUpdatedData, CoreEventSiteUpdatedData,
} from '@singletons/events'; } from '@singletons/events';
import { Network, NgZone, Platform } from '@singletons'; import { Network, NgZone, Platform, SplashScreen } from '@singletons';
import { CoreApp } from '@services/app'; import { CoreApp } from '@services/app';
import { CoreSites } from '@services/sites'; import { CoreSites } from '@services/sites';
import { CoreNavigator } from '@services/navigator'; import { CoreNavigator } from '@services/navigator';
import { CoreSubscriptions } from '@singletons/subscriptions';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
templateUrl: 'app.component.html', templateUrl: 'app.component.html',
styleUrls: ['app.component.scss'], styleUrls: ['app.component.scss'],
}) })
export class AppComponent implements OnInit { export class AppComponent implements OnInit, AfterViewInit {
constructor( @ViewChild(IonRouterOutlet) outlet?: IonRouterOutlet;
protected langProvider: CoreLangProvider,
protected loginHelper: CoreLoginHelperProvider,
) {
}
/** /**
* Component being initialized. * Component being initialized.
@ -58,7 +56,7 @@ export class AppComponent implements OnInit {
CoreNavigator.instance.navigate('/login/sites', { reset: true }); CoreNavigator.instance.navigate('/login/sites', { reset: true });
// Unload lang custom strings. // Unload lang custom strings.
this.langProvider.clearCustomStrings(); CoreLang.instance.clearCustomStrings();
// Remove version classes from body. // Remove version classes from body.
this.removeVersionClass(); this.removeVersionClass();
@ -66,20 +64,20 @@ export class AppComponent implements OnInit {
// Listen for session expired events. // Listen for session expired events.
CoreEvents.on(CoreEvents.SESSION_EXPIRED, (data: CoreEventSessionExpiredData) => { CoreEvents.on(CoreEvents.SESSION_EXPIRED, (data: CoreEventSessionExpiredData) => {
this.loginHelper.sessionExpired(data); CoreLoginHelper.instance.sessionExpired(data);
}); });
// Listen for passwordchange and usernotfullysetup events to open InAppBrowser. // Listen for passwordchange and usernotfullysetup events to open InAppBrowser.
CoreEvents.on(CoreEvents.PASSWORD_CHANGE_FORCED, (data: CoreEventSiteData) => { 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) => { 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. // Listen for sitepolicynotagreed event to accept the site policy.
CoreEvents.on(CoreEvents.SITE_POLICY_NOT_AGREED, (data: CoreEventSiteData) => { 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) => { CoreEvents.on(CoreEvents.LOGIN, async (data: CoreEventSiteData) => {
@ -119,6 +117,17 @@ export class AppComponent implements OnInit {
this.onPlatformReady(); this.onPlatformReady();
} }
/**
* @inheritdoc
*/
ngAfterViewInit(): void {
if (!this.outlet) {
return;
}
CoreSubscriptions.once(this.outlet.activateEvents, () => SplashScreen.instance.hide());
}
/** /**
* Async init function on platform ready. * Async init function on platform ready.
*/ */
@ -155,8 +164,9 @@ export class AppComponent implements OnInit {
*/ */
protected loadCustomStrings(): void { protected loadCustomStrings(): void {
const currentSite = CoreSites.instance.getCurrentSite(); const currentSite = CoreSites.instance.getCurrentSite();
if (currentSite) { if (currentSite) {
this.langProvider.loadCustomStringsFromSite(currentSite); CoreLang.instance.loadCustomStringsFromSite(currentSite);
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

View File

@ -38,6 +38,7 @@ import { CoreLinkDirective } from './link';
import { CoreFilter, CoreFilterFilter, CoreFilterFormatTextOptions } from '@features/filter/services/filter'; import { CoreFilter, CoreFilterFilter, CoreFilterFormatTextOptions } from '@features/filter/services/filter';
import { CoreFilterDelegate } from '@features/filter/services/filter-delegate'; import { CoreFilterDelegate } from '@features/filter/services/filter-delegate';
import { CoreFilterHelper } from '@features/filter/services/filter-helper'; 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 * 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 Promise.resolve();
} }
return new Promise((resolve): void => { return new Promise(resolve => CoreSubscriptions.once(externalImage.onLoad, resolve));
const subscription = externalImage.onLoad.subscribe(() => {
subscription.unsubscribe();
resolve();
});
});
})); }));
// Automatically reject the promise after 5 seconds to prevent blocking the user forever. // Automatically reject the promise after 5 seconds to prevent blocking the user forever.

View 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;
}
}

View File

@ -21,16 +21,13 @@ import { TranslateModule } from '@ngx-translate/core';
import { CoreSharedModule } from '@/core/shared.module'; import { CoreSharedModule } from '@/core/shared.module';
import { CoreLoginSiteHelpComponent } from './components/site-help/site-help'; import { CoreLoginSiteHelpComponent } from './components/site-help/site-help';
import { CoreLoginSiteOnboardingComponent } from './components/site-onboarding/site-onboarding'; import { CoreLoginSiteOnboardingComponent } from './components/site-onboarding/site-onboarding';
import { CoreLoginHasSitesGuard } from './guards/has-sites';
const routes: Routes = [ const routes: Routes = [
{ {
path: '', path: '',
redirectTo: 'init',
pathMatch: 'full', pathMatch: 'full',
}, redirectTo: 'sites',
{
path: 'init',
loadChildren: () => import('./pages/init/init.module').then( m => m.CoreLoginInitPageModule),
}, },
{ {
path: 'site', path: 'site',
@ -43,6 +40,8 @@ const routes: Routes = [
{ {
path: 'sites', path: 'sites',
loadChildren: () => import('./pages/sites/sites.module').then( m => m.CoreLoginSitesPageModule), loadChildren: () => import('./pages/sites/sites.module').then( m => m.CoreLoginSitesPageModule),
canLoad: [CoreLoginHasSitesGuard],
canActivate: [CoreLoginHasSitesGuard],
}, },
{ {
path: 'forgottenpassword', path: 'forgottenpassword',

View File

@ -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>

View File

@ -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));
}
}

View File

@ -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 });
}
}

View File

@ -45,13 +45,7 @@ export class CoreLoginSitesPage implements OnInit {
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
async ngOnInit(): Promise<void> { async ngOnInit(): Promise<void> {
const sites = await CoreUtils.instance.ignoreErrors(CoreSites.instance.getSortedSites()); const sites = await CoreUtils.instance.ignoreErrors(CoreSites.instance.getSortedSites(), [] as CoreSiteBasicInfo[]);
if (!sites || sites.length == 0) {
CoreLoginHelper.instance.goToAddSite(true);
return;
}
// Remove protocol from the url to show more url text. // Remove protocol from the url to show more url text.
this.sites = sites.map((site) => { this.sites = sites.map((site) => {

View File

@ -34,6 +34,7 @@ import { makeSingleton, Translate } from '@singletons';
import { CoreLogger } from '@singletons/logger'; import { CoreLogger } from '@singletons/logger';
import { CoreUrl } from '@singletons/url'; import { CoreUrl } from '@singletons/url';
import { CoreNavigator } from '@services/navigator'; import { CoreNavigator } from '@services/navigator';
import { CoreObject } from '@singletons/object';
/** /**
* Helper provider that provides some common features regarding authentication. * Helper provider that provides some common features regarding authentication.
@ -408,22 +409,27 @@ export class CoreLoginHelperProvider {
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
async goToAddSite(setRoot?: boolean, showKeyboard?: boolean): Promise<void> { async goToAddSite(setRoot?: boolean, showKeyboard?: boolean): Promise<void> {
let pageRoute: string; const [path, params] = this.getAddSiteRouteInfo(showKeyboard);
let params: Params;
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()) { if (this.isFixedUrlSet()) {
// Fixed URL is set, go to credentials page. // Fixed URL is set, go to credentials page.
const fixedSites = this.getFixedSites(); const fixedSites = this.getFixedSites();
const url = typeof fixedSites == 'string' ? fixedSites : fixedSites[0].url; const url = typeof fixedSites == 'string' ? fixedSites : fixedSites[0].url;
pageRoute = '/login/credentials'; return ['/login/credentials', { siteUrl: url }];
params = { siteUrl: url };
} else {
pageRoute = '/login/site';
params = { showKeyboard: showKeyboard };
} }
await CoreNavigator.instance.navigate(pageRoute, { params, reset: setRoot }); return ['/login/site', CoreObject.withoutEmpty({ showKeyboard: showKeyboard })];
} }
/** /**

View File

@ -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 });
});
});

View File

@ -13,28 +13,44 @@
// limitations under the License. // limitations under the License.
import { Injectable } from '@angular/core'; 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 { CoreSites } from '@services/sites';
import { ApplicationInit } from '@singletons'; import { Router } from '@singletons';
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class AuthGuard implements CanLoad, CanActivate { export class CoreMainMenuAuthGuard implements CanLoad, CanActivate {
constructor(private router: Router) {}
/**
* @inheritdoc
*/
canActivate(): Promise<true | UrlTree> { canActivate(): Promise<true | UrlTree> {
return this.guard(); return this.guard();
} }
/**
* @inheritdoc
*/
canLoad(): Promise<true | UrlTree> { canLoad(): Promise<true | UrlTree> {
return this.guard(); return this.guard();
} }
/**
* Check if the current user should be redirected to the authentication page.
*/
private async guard(): Promise<true | UrlTree> { 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;
} }
} }

View File

@ -14,7 +14,7 @@
import { APP_INITIALIZER, NgModule } from '@angular/core'; import { APP_INITIALIZER, NgModule } from '@angular/core';
import { Routes } from '@angular/router'; import { Routes } from '@angular/router';
import { AuthGuard } from '@guards/auth'; import { CoreMainMenuAuthGuard } from '@features/mainmenu/guards/auth';
import { AppRoutingModule } from '@/app/app-routing.module'; import { AppRoutingModule } from '@/app/app-routing.module';
@ -30,8 +30,8 @@ const appRoutes: Routes = [
{ {
path: 'main', path: 'main',
loadChildren: () => import('./mainmenu-lazy.module').then(m => m.CoreMainMenuLazyModule), loadChildren: () => import('./mainmenu-lazy.module').then(m => m.CoreMainMenuLazyModule),
canActivate: [AuthGuard], canActivate: [CoreMainMenuAuthGuard],
canLoad: [AuthGuard], canLoad: [CoreMainMenuAuthGuard],
}, },
]; ];

View 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();
}
}
}

View File

@ -12,27 +12,8 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { NgModule } from '@angular/core'; import { CoreApp } from '@services/app';
import { RouterModule, Routes } from '@angular/router';
import { IonicModule } from '@ionic/angular';
import { CoreLoginInitPage } from './init'; export default function(): void {
CoreApp.instance.consumeStorageRedirect();
const routes: Routes = [ }
{
path: '',
component: CoreLoginInitPage,
},
];
@NgModule({
imports: [
RouterModule.forChild(routes),
IonicModule,
],
declarations: [
CoreLoginInitPage,
],
exports: [RouterModule],
})
export class CoreLoginInitPageModule {}

View File

@ -25,6 +25,7 @@ import { makeSingleton, Keyboard, Network, StatusBar, Platform, Device } from '@
import { CoreLogger } from '@singletons/logger'; import { CoreLogger } from '@singletons/logger';
import { CoreColors } from '@singletons/colors'; import { CoreColors } from '@singletons/colors';
import { DBNAME, SCHEMA_VERSIONS_TABLE_NAME, SCHEMA_VERSIONS_TABLE_SCHEMA, SchemaVersionsDBEntry } from '@services/database/app'; 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. * Object responsible of managing schema versions.
@ -58,6 +59,7 @@ export class CoreAppProvider {
protected keyboardClosing = false; protected keyboardClosing = false;
protected backActions: {callback: () => boolean; priority: number}[] = []; protected backActions: {callback: () => boolean; priority: number}[] = [];
protected forceOffline = false; protected forceOffline = false;
protected redirect?: CoreRedirectData;
// Variables for DB. // Variables for DB.
protected schemaVersionsManager: Promise<SchemaVersionsManager>; protected schemaVersionsManager: Promise<SchemaVersionsManager>;
@ -516,32 +518,50 @@ export class CoreAppProvider {
await deferred.promise; 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. * Retrieve redirect data.
* *
* @return Object with siteid, state, params and timemodified. * @return Object with siteid, state, params and timemodified.
*/ */
getRedirect(): CoreRedirectData { getRedirect(): CoreRedirectData | null {
if (localStorage?.getItem) { return this.redirect || null;
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 {};
} }
/** /**
@ -552,17 +572,19 @@ export class CoreAppProvider {
* @param params Page params. * @param params Page params.
*/ */
storeRedirect(siteId: string, page: string, params: Params): void { storeRedirect(siteId: string, page: string, params: Params): void {
if (localStorage && localStorage.setItem) {
try { try {
localStorage.setItem('CoreRedirectSiteId', siteId); const redirect: CoreRedirectData = {
localStorage.setItem('CoreRedirectState', page); siteId,
localStorage.setItem('CoreRedirectParams', JSON.stringify(params)); page,
localStorage.setItem('CoreRedirectTime', String(Date.now())); params,
timemodified: Date.now(),
};
localStorage.setItem('CoreRedirect', JSON.stringify(redirect));
} catch (ex) { } catch (ex) {
// Ignore errors. // Ignore errors.
} }
} }
}
/** /**
* The back button event is triggered when the user presses the native * The back button event is triggered when the user presses the native

View File

@ -18,6 +18,7 @@ import { CoreConstants } from '@/core/constants';
import { LangChangeEvent } from '@ngx-translate/core'; import { LangChangeEvent } from '@ngx-translate/core';
import { CoreAppProvider } from '@services/app'; import { CoreAppProvider } from '@services/app';
import { CoreConfig } from '@services/config'; import { CoreConfig } from '@services/config';
import { CoreSubscriptions } from '@singletons/subscriptions';
import { makeSingleton, Translate, Platform } from '@singletons'; import { makeSingleton, Translate, Platform } from '@singletons';
import * as moment from 'moment'; import * as moment from 'moment';
@ -128,44 +129,25 @@ export class CoreLangProvider {
// Change the language, resolving the promise when we receive the first value. // Change the language, resolving the promise when we receive the first value.
promises.push(new Promise((resolve, reject) => { 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. // It's a language override, load the original one first.
const fallbackLang = Translate.instance.instant('core.parentlanguage'); const fallbackLang = Translate.instance.instant('core.parentlanguage');
if (fallbackLang != '' && fallbackLang != 'core.parentlanguage' && fallbackLang != language) { if (fallbackLang != '' && fallbackLang != 'core.parentlanguage' && fallbackLang != language) {
const fallbackSubs = Translate.instance.use(fallbackLang).subscribe((fallbackData) => { CoreSubscriptions.once(
Translate.instance.use(fallbackLang),
fallbackData => {
data = Object.assign(fallbackData, data); data = Object.assign(fallbackData, data);
resolve(data);
// Data received, unsubscribe. Use a timeout because we can receive a value immediately. resolve(data);
setTimeout(() => { },
fallbackSubs.unsubscribe();
});
}, () => {
// Resolve with the original language. // Resolve with the original language.
resolve(data); () => resolve(data),
);
// Error received, unsubscribe. Use a timeout because we can receive a value immediately.
setTimeout(() => {
fallbackSubs.unsubscribe();
});
});
} else { } else {
resolve(data); resolve(data);
} }
}, reject);
// 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();
});
});
})); }));
// Change the config. // Change the config.

View 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);
},
);
}
}