diff --git a/src/app/app.component.ts b/src/app/app.component.ts
index 3296ee02d..8ddb2387a 100644
--- a/src/app/app.component.ts
+++ b/src/app/app.component.ts
@@ -16,7 +16,8 @@ import { Component, OnInit } from '@angular/core';
import { NavController } from '@ionic/angular';
import { CoreLangProvider } from '@services/lang';
-import { CoreEvents } from '@singletons/events';
+import { CoreLoginHelperProvider } from '@core/login/services/helper';
+import { CoreEvents, CoreEventSessionExpiredData } from '@singletons/events';
@Component({
selector: 'app-root',
@@ -28,6 +29,7 @@ export class AppComponent implements OnInit {
constructor(
protected langProvider: CoreLangProvider,
protected navCtrl: NavController,
+ protected loginHelper: CoreLoginHelperProvider,
) {
}
@@ -46,6 +48,11 @@ export class AppComponent implements OnInit {
// @todo
// this.removeVersionClass();
});
+
+ // Listen for session expired events.
+ CoreEvents.on(CoreEvents.SESSION_EXPIRED, (data: CoreEventSessionExpiredData) => {
+ this.loginHelper.sessionExpired(data);
+ });
}
}
diff --git a/src/app/core/login/login-routing.module.ts b/src/app/core/login/login-routing.module.ts
index b123f3b93..95ff0c8e8 100644
--- a/src/app/core/login/login-routing.module.ts
+++ b/src/app/core/login/login-routing.module.ts
@@ -39,21 +39,25 @@ const routes: Routes = [
},
{
path: 'forgottenpassword',
- loadChildren: () => import('./pages/forgotten-password/forgotten-password.module')
+ loadChildren: () => import('./pages/forgotten-password/forgotten-password.page.module')
.then( m => m.CoreLoginForgottenPasswordPageModule),
},
{
path: 'changepassword',
- loadChildren: () => import('./pages/change-password/change-password.module')
+ loadChildren: () => import('./pages/change-password/change-password.page.module')
.then( m => m.CoreLoginChangePasswordPageModule),
},
{
path: 'sitepolicy',
- loadChildren: () => import('./pages/site-policy/site-policy.module').then( m => m.CoreLoginSitePolicyPageModule),
+ loadChildren: () => import('./pages/site-policy/site-policy.page.module').then( m => m.CoreLoginSitePolicyPageModule),
},
{
path: 'emailsignup',
- loadChildren: () => import('./pages/email-signup/email-signup.module').then( m => m.CoreLoginEmailSignupPageModule),
+ loadChildren: () => import('./pages/email-signup/email-signup.page.module').then( m => m.CoreLoginEmailSignupPageModule),
+ },
+ {
+ path: 'reconnect',
+ loadChildren: () => import('./pages/reconnect/reconnect.page.module').then( m => m.CoreLoginReconnectPageModule),
},
];
diff --git a/src/app/core/login/login.scss b/src/app/core/login/login.scss
index 6afeeed42..6e67854fa 100644
--- a/src/app/core/login/login.scss
+++ b/src/app/core/login/login.scss
@@ -13,7 +13,7 @@
}
.core-sitename {
- font-size: 1.8rem;
+ font-size: 1.2rem;
}
.core-login-site-logo {
diff --git a/src/app/core/login/pages/change-password/change-password.module.ts b/src/app/core/login/pages/change-password/change-password.page.module.ts
similarity index 100%
rename from src/app/core/login/pages/change-password/change-password.module.ts
rename to src/app/core/login/pages/change-password/change-password.page.module.ts
diff --git a/src/app/core/login/pages/credentials/credentials.page.ts b/src/app/core/login/pages/credentials/credentials.page.ts
index fd8bfeceb..3049a0245 100644
--- a/src/app/core/login/pages/credentials/credentials.page.ts
+++ b/src/app/core/login/pages/credentials/credentials.page.ts
@@ -28,7 +28,7 @@ import { CoreSiteIdentityProvider, CoreSitePublicConfigResponse } from '@classes
import { CoreEvents } from '@singletons/events';
/**
- * Page that displays a "splash screen" while the app is being initialized.
+ * Page to enter the user credentials.
*/
@Component({
selector: 'page-core-login-credentials',
@@ -161,7 +161,7 @@ export class CoreLoginCredentialsPage implements OnInit, OnDestroy {
*/
protected treatSiteConfig(): void {
if (this.siteConfig) {
- this.siteName = CoreConstants.CONFIG.sitename ?? this.siteConfig.sitename;
+ this.siteName = CoreConstants.CONFIG.sitename ? CoreConstants.CONFIG.sitename : this.siteConfig.sitename;
this.logoUrl = CoreLoginHelper.instance.getLogoUrl(this.siteConfig);
this.authInstructions = this.siteConfig.authinstructions || Translate.instance.instant('core.login.loginsteps');
diff --git a/src/app/core/login/pages/email-signup/email-signup.module.ts b/src/app/core/login/pages/email-signup/email-signup.page.module.ts
similarity index 100%
rename from src/app/core/login/pages/email-signup/email-signup.module.ts
rename to src/app/core/login/pages/email-signup/email-signup.page.module.ts
diff --git a/src/app/core/login/pages/forgotten-password/forgotten-password.module.ts b/src/app/core/login/pages/forgotten-password/forgotten-password.page.module.ts
similarity index 100%
rename from src/app/core/login/pages/forgotten-password/forgotten-password.module.ts
rename to src/app/core/login/pages/forgotten-password/forgotten-password.page.module.ts
diff --git a/src/app/core/login/pages/reconnect/reconnect.html b/src/app/core/login/pages/reconnect/reconnect.html
new file mode 100644
index 000000000..add46b614
--- /dev/null
+++ b/src/app/core/login/pages/reconnect/reconnect.html
@@ -0,0 +1,90 @@
+
+
+
+
+
+
+ {{ 'core.login.reconnect' | translate }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
![]()
+

+
+
+
+
+
+
{{siteUrl}}
+
+
+
+
+ {{ 'core.login.reconnectdescription' | translate }}
+
+
+
+
+
+
+
+
+
+ {{ 'core.login.forgotten' | translate }}
+
+
+
+
+
+
+
+ {{ 'core.login.potentialidps' | translate }}
+
+
+
+ {{provider.name}}
+
+
+
+
+
+
+ {{ 'core.login.cancel' | translate }}
+
+
+
diff --git a/src/app/core/login/pages/reconnect/reconnect.page.module.ts b/src/app/core/login/pages/reconnect/reconnect.page.module.ts
new file mode 100644
index 000000000..fb970ca28
--- /dev/null
+++ b/src/app/core/login/pages/reconnect/reconnect.page.module.ts
@@ -0,0 +1,50 @@
+// (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 { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
+import { RouterModule, Routes } from '@angular/router';
+import { IonicModule } from '@ionic/angular';
+import { TranslateModule } from '@ngx-translate/core';
+
+import { CoreComponentsModule } from '@components/components.module';
+import { CoreDirectivesModule } from '@directives/directives.module';
+
+import { CoreLoginReconnectPage } from './reconnect.page';
+
+const routes: Routes = [
+ {
+ path: '',
+ component: CoreLoginReconnectPage,
+ },
+];
+
+@NgModule({
+ imports: [
+ RouterModule.forChild(routes),
+ CommonModule,
+ IonicModule,
+ TranslateModule.forChild(),
+ FormsModule,
+ ReactiveFormsModule,
+ CoreComponentsModule,
+ CoreDirectivesModule,
+ ],
+ declarations: [
+ CoreLoginReconnectPage,
+ ],
+ exports: [RouterModule],
+})
+export class CoreLoginReconnectPageModule {}
diff --git a/src/app/core/login/pages/reconnect/reconnect.page.ts b/src/app/core/login/pages/reconnect/reconnect.page.ts
new file mode 100644
index 000000000..3aea87e3b
--- /dev/null
+++ b/src/app/core/login/pages/reconnect/reconnect.page.ts
@@ -0,0 +1,245 @@
+// (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, OnDestroy, ViewChild, ElementRef } from '@angular/core';
+import { ActivatedRoute, Params } from '@angular/router';
+import { FormBuilder, FormGroup, Validators } from '@angular/forms';
+import { NavController } from '@ionic/angular';
+
+import { CoreApp } from '@services/app';
+import { CoreSites } from '@services/sites';
+import { CoreDomUtils } from '@services/utils/dom';
+import { CoreUtils } from '@services/utils/utils';
+import { CoreLoginHelper } from '@core/login/services/helper';
+import { CoreSiteIdentityProvider, CoreSitePublicConfigResponse } from '@classes/site';
+import { CoreEvents } from '@singletons/events';
+import { CoreError } from '@classes/errors/error';
+
+/**
+ * Page to enter the user password to reconnect to a site.
+ */
+@Component({
+ selector: 'page-core-login-reconnect',
+ templateUrl: 'reconnect.html',
+ styleUrls: ['../../login.scss'],
+})
+export class CoreLoginReconnectPage implements OnInit, OnDestroy {
+
+ @ViewChild('reconnectForm') formElement?: ElementRef;
+
+ credForm: FormGroup;
+ siteUrl!: string;
+ username!: string;
+ userFullName!: string;
+ userAvatar?: string;
+ siteName!: string;
+ logoUrl?: string;
+ identityProviders?: CoreSiteIdentityProvider[];
+ showForgottenPassword = true;
+ showSiteAvatar = false;
+ isOAuth = false;
+ isLoggedOut: boolean;
+ siteId!: string;
+
+ protected page?: string;
+ protected pageParams?: Params;
+ protected siteConfig?: CoreSitePublicConfigResponse;
+ protected viewLeft = false;
+ protected eventThrown = false;
+
+ constructor(
+ protected navCtrl: NavController,
+ protected fb: FormBuilder,
+ protected route: ActivatedRoute,
+ ) {
+
+ const currentSite = CoreSites.instance.getCurrentSite();
+
+ this.isLoggedOut = !!currentSite?.isLoggedOut();
+ this.credForm = fb.group({
+ password: ['', Validators.required],
+ });
+ }
+
+ /**
+ * Initialize the component.
+ */
+ async ngOnInit(): Promise {
+ const params = this.route.snapshot.queryParams;
+
+ this.siteId = params['siteId'];
+ this.page = params['pageName'];
+ this.pageParams = params['pageParams'];
+
+ try {
+ const site = await CoreSites.instance.getSite(this.siteId);
+
+ if (!site.infos) {
+ throw new CoreError('Invalid site');
+ }
+
+ this.username = site.infos.username;
+ this.userFullName = site.infos.fullname;
+ this.userAvatar = site.infos.userpictureurl;
+ this.siteUrl = site.infos.siteurl;
+ this.siteName = site.getSiteName();
+
+ // If login was OAuth we should only reach this page if the OAuth method ID has changed.
+ this.isOAuth = site.isOAuth();
+
+ // Show logo instead of avatar if it's a fixed site.
+ this.showSiteAvatar = !!this.userAvatar && !CoreLoginHelper.instance.getFixedSites();
+
+ const config = await CoreUtils.instance.ignoreErrors(site.getPublicConfig());
+
+ if (!config) {
+ return;
+ }
+
+ this.siteConfig = config;
+
+ await CoreSites.instance.checkRequiredMinimumVersion(config);
+
+ // Check logoURL if user avatar is not set.
+ if (this.userAvatar.startsWith(this.siteUrl + '/theme/image.php')) {
+ this.showSiteAvatar = false;
+ }
+ this.logoUrl = CoreLoginHelper.instance.getLogoUrl(config);
+
+ this.getDataFromConfig(this.siteConfig);
+ } catch (error) {
+ // Just leave the view.
+ this.cancel();
+ }
+ }
+
+ /**
+ * Component destroyed.
+ */
+ ngOnDestroy(): void {
+ this.viewLeft = true;
+ CoreEvents.trigger(CoreEvents.LOGIN_SITE_UNCHECKED, { config: this.siteConfig }, this.siteId);
+ }
+
+ /**
+ * Get some data (like identity providers) from the site config.
+ *
+ * @param config Config to use.
+ */
+ protected getDataFromConfig(config: CoreSitePublicConfigResponse): void {
+ const disabledFeatures = CoreLoginHelper.instance.getDisabledFeatures(config);
+
+ this.identityProviders = CoreLoginHelper.instance.getValidIdentityProviders(config, disabledFeatures);
+ this.showForgottenPassword = !CoreLoginHelper.instance.isForgottenPasswordDisabled(config);
+
+ if (!this.eventThrown && !this.viewLeft) {
+ this.eventThrown = true;
+ CoreEvents.trigger(CoreEvents.LOGIN_SITE_CHECKED, { config: config });
+ }
+ }
+
+ /**
+ * Cancel reconnect.
+ *
+ * @param e Event.
+ */
+ cancel(e?: Event): void {
+ if (e) {
+ e.preventDefault();
+ e.stopPropagation();
+ }
+
+ CoreSites.instance.logout();
+ }
+
+ /**
+ * Tries to authenticate the user.
+ *
+ * @param e Event.
+ */
+ async login(e: Event): Promise {
+ e.preventDefault();
+ e.stopPropagation();
+
+ CoreApp.instance.closeKeyboard();
+
+ // Get input data.
+ const password = this.credForm.value.password;
+
+ if (!password) {
+ CoreDomUtils.instance.showErrorModal('core.login.passwordrequired', true);
+
+ return;
+ }
+
+ if (!CoreApp.instance.isOnline()) {
+ CoreDomUtils.instance.showErrorModal('core.networkerrormsg', true);
+
+ return;
+ }
+
+ const modal = await CoreDomUtils.instance.showModalLoading();
+
+ try {
+ // Start the authentication process.
+ const data = await CoreSites.instance.getUserToken(this.siteUrl, this.username, password);
+
+ await CoreSites.instance.updateSiteToken(this.siteUrl, this.username, data.token, data.privateToken);
+
+ CoreDomUtils.instance.triggerFormSubmittedEvent(this.formElement, true);
+
+ // Update site info too.
+ await CoreSites.instance.updateSiteInfoByUrl(this.siteUrl, this.username);
+
+ // Reset fields so the data is not in the view anymore.
+ this.credForm.controls['password'].reset();
+
+ // Go to the site initial page.
+ await CoreLoginHelper.instance.goToSiteInitialPage({
+ redirectPage: this.page,
+ redirectParams: this.pageParams,
+ });
+ } catch (error) {
+ CoreLoginHelper.instance.treatUserTokenError(this.siteUrl, error, this.username, password);
+
+ if (error.loggedout) {
+ this.cancel();
+ } else if (error.errorcode == 'forcepasswordchangenotice') {
+ // Reset password field.
+ this.credForm.controls.password.reset();
+ }
+ } finally {
+ modal.dismiss();
+ }
+ }
+
+ /**
+ * Forgotten password button clicked.
+ */
+ forgottenPassword(): void {
+ CoreLoginHelper.instance.forgottenPasswordClicked(this.siteUrl, this.username, this.siteConfig);
+ }
+
+ /**
+ * An OAuth button was clicked.
+ *
+ * @param provider The provider that was clicked.
+ */
+ oauthClicked(provider: CoreSiteIdentityProvider): void {
+ if (!CoreLoginHelper.instance.openBrowserForOAuthLogin(this.siteUrl, provider, this.siteConfig?.launchurl)) {
+ CoreDomUtils.instance.showErrorModal('Invalid data.');
+ }
+ }
+
+}
diff --git a/src/app/core/login/pages/site-policy/site-policy.module.ts b/src/app/core/login/pages/site-policy/site-policy.page.module.ts
similarity index 100%
rename from src/app/core/login/pages/site-policy/site-policy.module.ts
rename to src/app/core/login/pages/site-policy/site-policy.page.module.ts
diff --git a/src/app/core/login/services/helper.ts b/src/app/core/login/services/helper.ts
index de0026857..126f29958 100644
--- a/src/app/core/login/services/helper.ts
+++ b/src/app/core/login/services/helper.ts
@@ -35,6 +35,7 @@ import { makeSingleton, Translate } from '@singletons/core.singletons';
import { CoreLogger } from '@singletons/logger';
import { CoreUrl } from '@singletons/url';
import { NavigationOptions } from '@ionic/angular/providers/nav-controller';
+import { CoreObject } from '@/app/singletons/object';
/**
* Helper provider that provides some common features regarding authentication.
@@ -126,7 +127,7 @@ export class CoreLoginHelperProvider {
const currentSite = CoreSites.instance.getCurrentSite();
const currentPage = CoreApp.instance.getCurrentPage();
- if (!CoreApp.instance.isSSOAuthenticationOngoing() && currentSite?.isLoggedOut() && currentPage == 'login/reconnect') {
+ if (!CoreApp.instance.isSSOAuthenticationOngoing() && currentSite?.isLoggedOut() && currentPage == '/login/reconnect') {
// User must reauthenticate but he closed the InAppBrowser without doing so, logout him.
CoreSites.instance.logout();
}
@@ -1106,14 +1107,11 @@ export class CoreLoginHelperProvider {
this.isOpeningReconnect = true;
await CoreUtils.instance.ignoreErrors(this.navCtrl.navigateRoot('/login/reconnect', {
- queryParams: {
- infoSiteUrl: info.siteurl,
- siteUrl: result.siteUrl,
- siteId: siteId,
+ queryParams: CoreObject.removeUndefined({
+ siteId,
pageName: data.pageName,
pageParams: data.params,
- siteConfig: result.config,
- },
+ }),
}));
this.isOpeningReconnect = false;
diff --git a/src/app/singletons/object.ts b/src/app/singletons/object.ts
new file mode 100644
index 000000000..ac908aa09
--- /dev/null
+++ b/src/app/singletons/object.ts
@@ -0,0 +1,35 @@
+// (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.
+
+/**
+ * Singleton with helper functions for objects.
+ */
+export class CoreObject {
+
+ /**
+ * Delete all keys from an object whose value are null or undefined.
+ *
+ * @param object Object to modify.
+ */
+ static removeUndefined(object: T): T {
+ for (const name in object) {
+ if (object[name] === undefined) {
+ delete object[name];
+ }
+ }
+
+ return object;
+ }
+
+}