diff --git a/scripts/langindex.json b/scripts/langindex.json index db523decd..9e6a56dc5 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -2168,6 +2168,7 @@ "core.login.missingfirstname": "moodle", "core.login.missinglastname": "moodle", "core.login.mobileservicesnotenabled": "local_moodlemobileapp", + "core.login.morewaystologin": "local_moodlemobileapp", "core.login.mustconfirm": "moodle", "core.login.newaccount": "moodle", "core.login.notloggedin": "local_moodlemobileapp", diff --git a/src/core/classes/sites/site.ts b/src/core/classes/sites/site.ts index dbf13a114..296b5d9d0 100644 --- a/src/core/classes/sites/site.ts +++ b/src/core/classes/sites/site.ts @@ -195,6 +195,7 @@ export class CoreSite extends CoreAuthenticatedSite { * Check if the user authenticated in the site using an OAuth method. * * @returns Whether the user authenticated in the site using an OAuth method. + * @deprecated since 5.0. Use getOAuthId instead. */ isOAuth(): boolean { return this.oauthId != null && this.oauthId !== undefined; diff --git a/src/core/features/login/components/components.module.ts b/src/core/features/login/components/components.module.ts index 1d2fc3fa7..be46f2bfd 100644 --- a/src/core/features/login/components/components.module.ts +++ b/src/core/features/login/components/components.module.ts @@ -16,6 +16,7 @@ import { NgModule } from '@angular/core'; import { CoreSharedModule } from '@/core/shared.module'; import { CoreLoginMethodsComponent } from './login-methods/login-methods'; import { CoreLoginExceededAttemptsComponent } from '@features/login/components/exceeded-attempts/exceeded-attempts'; +import { CoreLoginIdentityProviderComponent } from './identity-provider/identity-provider'; @NgModule({ declarations: [ @@ -24,6 +25,7 @@ import { CoreLoginExceededAttemptsComponent } from '@features/login/components/e ], imports: [ CoreSharedModule, + CoreLoginIdentityProviderComponent, ], exports: [ CoreLoginExceededAttemptsComponent, diff --git a/src/core/features/login/components/identity-provider/identity-provider.html b/src/core/features/login/components/identity-provider/identity-provider.html new file mode 100644 index 000000000..e36769e20 --- /dev/null +++ b/src/core/features/login/components/identity-provider/identity-provider.html @@ -0,0 +1,7 @@ + + @if (provider.iconurl) { + + } + {{ provider.name }} + diff --git a/src/core/features/login/components/identity-provider/identity-provider.ts b/src/core/features/login/components/identity-provider/identity-provider.ts new file mode 100644 index 000000000..350ef4c72 --- /dev/null +++ b/src/core/features/login/components/identity-provider/identity-provider.ts @@ -0,0 +1,53 @@ +// (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, Input } from '@angular/core'; +import { CoreSiteIdentityProvider } from '@classes/sites/unauthenticated-site'; +import { CoreLoginHelper } from '@features/login/services/login-helper'; +import { CoreDomUtils } from '@services/utils/dom'; +import { CoreSharedModule } from '@/core/shared.module'; +import { CoreRedirectPayload } from '@services/navigator'; + +@Component({ + selector: 'core-identity-provider', + templateUrl: 'identity-provider.html', + standalone: true, + imports: [ + CoreSharedModule, + ], +}) +export class CoreLoginIdentityProviderComponent { + + @Input({ required: true }) provider!: CoreSiteIdentityProvider; + @Input() launchurl = ''; + @Input() siteUrl = ''; + @Input() redirectData?: CoreRedirectPayload; + + /** + * The button has been clicked. + */ + async openOAuth(): Promise { + const result = await CoreLoginHelper.openBrowserForOAuthLogin( + this.siteUrl, + this.provider, + this.launchurl, + this.redirectData, + ); + + if (!result) { + CoreDomUtils.showErrorModal('Invalid data.'); + } + } + +} diff --git a/src/core/features/login/components/login-methods/login-methods.html b/src/core/features/login/components/login-methods/login-methods.html index be93e7972..163766bb9 100644 --- a/src/core/features/login/components/login-methods/login-methods.html +++ b/src/core/features/login/components/login-methods/login-methods.html @@ -21,9 +21,6 @@

{{ 'core.login.potentialidps' | translate }}

- - - {{ provider.name }} - +
diff --git a/src/core/features/login/components/login-methods/login-methods.ts b/src/core/features/login/components/login-methods/login-methods.ts index 9b8f3b1a5..9d55d49fc 100644 --- a/src/core/features/login/components/login-methods/login-methods.ts +++ b/src/core/features/login/components/login-methods/login-methods.ts @@ -12,14 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { toBoolean } from '@/core/transforms/boolean'; import { Component, Input, OnInit } from '@angular/core'; +import { CorePromisedValue } from '@classes/promised-value'; +import { CoreSite } from '@classes/sites/site'; import { CoreSiteIdentityProvider, CoreSitePublicConfigResponse } from '@classes/sites/unauthenticated-site'; import { CoreLoginHelper, CoreLoginMethod } from '@features/login/services/login-helper'; import { CoreRedirectPayload } from '@services/navigator'; -import { CoreSites } from '@services/sites'; import { CoreSitesFactory } from '@services/sites-factory'; -import { CoreDomUtils } from '@services/utils/dom'; @Component({ selector: 'core-login-methods', @@ -28,10 +27,10 @@ import { CoreDomUtils } from '@services/utils/dom'; }) export class CoreLoginMethodsComponent implements OnInit { - @Input({ transform: toBoolean }) reconnect = false; @Input() siteUrl = ''; @Input() siteConfig?: CoreSitePublicConfigResponse; @Input() redirectData?: CoreRedirectPayload; + @Input() site?: CoreSite; // Defined when the user is reconnecting. @Input() showLoginForm = true; isBrowserSSO = false; @@ -39,16 +38,20 @@ export class CoreLoginMethodsComponent implements OnInit { loginMethods: CoreLoginMethod[] = []; identityProviders: CoreSiteIdentityProvider[] = []; + protected currentLoginProvider?: CoreSiteIdentityProvider; + protected isReady = new CorePromisedValue(); + /** * @inheritdoc */ async ngOnInit(): Promise { - if (this.reconnect) { + if (this.site) { + this.siteUrl = this.site.getURL(); + this.loginMethods = await CoreLoginHelper.getLoginMethods(); - const currentSite = CoreSites.getCurrentSite(); const defaultMethod = await CoreLoginHelper.getDefaultLoginMethod(); - if (currentSite?.isLoggedOut() && defaultMethod) { + if (this.site.isLoggedOut() && defaultMethod) { await defaultMethod.action(); } } @@ -59,25 +62,29 @@ export class CoreLoginMethodsComponent implements OnInit { // Identity providers won't be shown if login on browser. if (!this.isBrowserSSO) { this.identityProviders = await CoreLoginHelper.getValidIdentityProvidersForSite( - CoreSitesFactory.makeUnauthenticatedSite(this.siteUrl, this.siteConfig), + this.site ?? CoreSitesFactory.makeUnauthenticatedSite(this.siteUrl, this.siteConfig), ); } - if (this.reconnect) { + if (this.site) { this.showScanQR = CoreLoginHelper.displayQRInSiteScreen(); + + // The identity provider set in the site will be shown at the top. + const oAuthId = this.site.getOAuthId(); + this.currentLoginProvider = CoreLoginHelper.findIdentityProvider(this.identityProviders, oAuthId); } // If still false or credentials screen. - if (!this.reconnect || !this.showScanQR) { + if (!this.site || !this.showScanQR) { this.showScanQR = await CoreLoginHelper.displayQRInCredentialsScreen(this.siteConfig.tool_mobile_qrcodetype); } } + + this.isReady.resolve(); } /** * Show instructions and scan QR code. - * - * @returns Promise resolved when done. */ async showInstructionsAndScanQR(): Promise { try { @@ -90,21 +97,33 @@ export class CoreLoginMethodsComponent implements OnInit { } /** - * An OAuth button was clicked. + * Get the current login, removing the identity provider from the list. * - * @param provider The provider that was clicked. + * @returns Current login. */ - async oauthClicked(provider: CoreSiteIdentityProvider): Promise { - const result = await CoreLoginHelper.openBrowserForOAuthLogin( - this.siteUrl, - provider, - this.siteConfig?.launchurl, - this.redirectData, - ); + async extractCurrentLogin(): Promise { + await this.isReady; - if (!result) { - CoreDomUtils.showErrorModal('Invalid data.'); + if (!this.currentLoginProvider) { + return; } + + // Remove the identity provider from the array. + this.identityProviders = this.identityProviders.filter((provider) => + provider.url !== this.currentLoginProvider?.url); + + const showOther = !!(this.showLoginForm || this.isBrowserSSO) && + !!(this.loginMethods.length || this.identityProviders.length || this.showScanQR); + + return { + provider: this.currentLoginProvider, + showOther, + }; } } + +export type CoreLoginMethodsCurrentLogin = { + provider: CoreSiteIdentityProvider; + showOther: boolean; +}; diff --git a/src/core/features/login/lang.json b/src/core/features/login/lang.json index 015fa398a..b84f1bd53 100644 --- a/src/core/features/login/lang.json +++ b/src/core/features/login/lang.json @@ -71,6 +71,7 @@ "missingfirstname": "Missing given name", "missinglastname": "Missing last name", "mobileservicesnotenabled": "Mobile services are not enabled on the site.", + "morewaystologin": "More ways to log in", "mustconfirm": "You need to confirm your account", "newaccount": "New account", "notloggedin": "You need to be logged in.", diff --git a/src/core/features/login/login-reconnect-lazy.module.ts b/src/core/features/login/login-reconnect-lazy.module.ts index e0696584c..1d02a530e 100644 --- a/src/core/features/login/login-reconnect-lazy.module.ts +++ b/src/core/features/login/login-reconnect-lazy.module.ts @@ -19,6 +19,7 @@ import { CoreSharedModule } from '@/core/shared.module'; import { CoreLoginComponentsModule } from '@features/login/components/components.module'; import { CoreLoginReconnectPage } from '@features/login/pages/reconnect/reconnect'; import { CoreSiteLogoComponent } from '@/core/components/site-logo/site-logo'; +import { CoreLoginIdentityProviderComponent } from './components/identity-provider/identity-provider'; const routes: Routes = [ { @@ -33,6 +34,7 @@ const routes: Routes = [ CoreSharedModule, CoreLoginComponentsModule, CoreSiteLogoComponent, + CoreLoginIdentityProviderComponent, ], declarations: [ CoreLoginReconnectPage, diff --git a/src/core/features/login/pages/reconnect/reconnect.html b/src/core/features/login/pages/reconnect/reconnect.html index cda292eb9..1a682cefc 100644 --- a/src/core/features/login/pages/reconnect/reconnect.html +++ b/src/core/features/login/pages/reconnect/reconnect.html @@ -51,41 +51,62 @@ + + + + + + + + + + + + + diff --git a/src/core/features/login/pages/reconnect/reconnect.ts b/src/core/features/login/pages/reconnect/reconnect.ts index 1db1c98f7..8ad3b8a7f 100644 --- a/src/core/features/login/pages/reconnect/reconnect.ts +++ b/src/core/features/login/pages/reconnect/reconnect.ts @@ -34,6 +34,7 @@ import { CoreSitePublicConfigResponse } from '@classes/sites/unauthenticated-sit import { ALWAYS_SHOW_LOGIN_FORM_CHANGED, FORGOTTEN_PASSWORD_FEATURE_NAME } from '@features/login/constants'; import { CoreKeyboard } from '@singletons/keyboard'; import { CoreLoadings } from '@services/loadings'; +import { CoreLoginMethodsComponent, CoreLoginMethodsCurrentLogin } from '@features/login/components/login-methods/login-methods'; /** * Page to enter the user password to reconnect to a site. @@ -46,6 +47,17 @@ import { CoreLoadings } from '@services/loadings'; export class CoreLoginReconnectPage implements OnInit, OnDestroy { @ViewChild('reconnectForm') formElement?: ElementRef; + @ViewChild(CoreLoginMethodsComponent) set loginMethods(loginMethods: CoreLoginMethodsComponent) { + if (loginMethods && !this.currentLogin) { + loginMethods.extractCurrentLogin().then(login => { + this.currentLogin = login; + + return; + }).catch(() => { + // Ignore errors. + }); + } + } credForm: FormGroup; site!: CoreSite; @@ -53,6 +65,7 @@ export class CoreLoginReconnectPage implements OnInit, OnDestroy { showForgottenPassword = true; showUserAvatar = false; isBrowserSSO = false; + currentLogin?: CoreLoginMethodsCurrentLogin; isLoggedOut: boolean; siteId!: string; siteInfo?: CoreSiteBasicInfo; @@ -252,16 +265,21 @@ export class CoreLoginReconnectPage implements OnInit, OnDestroy { const modal = await CoreLoadings.show(); + const url = this.site.getURL(); + try { // Start the authentication process. - const data = await CoreSites.getUserToken(this.site.getURL(), this.username, password); + const data = await CoreSites.getUserToken(url, this.username, password); - await CoreSites.updateSiteToken(this.site.getURL(), this.username, data.token, data.privateToken); + await CoreSites.updateSiteToken(url, this.username, data.token, data.privateToken); CoreForms.triggerFormSubmittedEvent(this.formElement, true); + // Unset oAuthID if it's set. + await CoreSites.removeSiteOauthId(this.siteId); + // Update site info too. - await CoreSites.updateSiteInfoByUrl(this.site.getURL(), this.username); + await CoreSites.updateSiteInfoByUrl(url, this.username); // Reset fields so the data is not in the view anymore. this.credForm.controls['password'].reset(); @@ -271,7 +289,7 @@ export class CoreLoginReconnectPage implements OnInit, OnDestroy { params: this.redirectData, }); } catch (error) { - CoreLoginHelper.treatUserTokenError(this.site.getURL(), error, this.username, password); + CoreLoginHelper.treatUserTokenError(url, error, this.username, password); if (error.loggedout) { this.cancel(); diff --git a/src/core/features/login/services/login-helper.ts b/src/core/features/login/services/login-helper.ts index 1db10e651..4cc29c9d0 100644 --- a/src/core/features/login/services/login-helper.ts +++ b/src/core/features/login/services/login-helper.ts @@ -402,6 +402,21 @@ export class CoreLoginHelperProvider { return validProviders; } + /** + * Finds an identity provider from a list of providers based on the given OAuth ID. + * + * @param providers Array of identity providers. + * @param oauthId The OAuth ID to match against the providers' URLs. + * @returns The identity provider that matches the given OAuth ID, or undefined if no match is found. + */ + findIdentityProvider(providers: CoreSiteIdentityProvider[], oauthId?: number): CoreSiteIdentityProvider | undefined { + if (!oauthId) { + return; + } + + return providers.find(provider => Number(CoreUrl.extractUrlParams(provider.url).id) === oauthId); + } + /** * Go to the page to add a new site. * If a fixed URL is configured, go to credentials instead. diff --git a/src/core/services/sites.ts b/src/core/services/sites.ts index a58978349..5706f4d30 100644 --- a/src/core/services/sites.ts +++ b/src/core/services/sites.ts @@ -1632,6 +1632,23 @@ export class CoreSitesProvider { await Promise.all(promises); } + /** + * Removes the OAuth ID for a given site. + * + * @param siteId The ID of the site to update. + */ + async removeSiteOauthId(siteId: string): Promise { + const site = await this.getSite(siteId); + + site.setOAuthId(undefined); + + const newData: Partial = { + oauthId: null, + }; + + await this.sitesTable.update(newData, { id: siteId }); + } + /** * Updates a site's info. *