MOBILE-3689 init: Replace /login/init with guards
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.
|
||||
|
|
|
@ -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],
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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…
Reference in New Issue