MOBILE-4680 login: Show the current oauth method to the top on reconnect
This commit is contained in:
		
							parent
							
								
									ae1c719f19
								
							
						
					
					
						commit
						6c132bd736
					
				| @ -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", | ||||
|  | ||||
| @ -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; | ||||
|  | ||||
| @ -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, | ||||
|  | ||||
| @ -0,0 +1,7 @@ | ||||
| <ion-button class="ion-text-wrap ion-margin core-oauth-provider" (click)="openOAuth()" [ariaLabel]="provider.name" expand="block" | ||||
|     fill="outline"> | ||||
|     @if (provider.iconurl) { | ||||
|     <img [src]="provider.iconurl" alt="" width="32" height="32" slot="start" aria-hidden="true" (error)="provider.iconurl = ''" /> | ||||
|     } | ||||
|     <ion-label>{{ provider.name }}</ion-label> | ||||
| </ion-button> | ||||
| @ -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<void> { | ||||
|         const result = await CoreLoginHelper.openBrowserForOAuthLogin( | ||||
|             this.siteUrl, | ||||
|             this.provider, | ||||
|             this.launchurl, | ||||
|             this.redirectData, | ||||
|         ); | ||||
| 
 | ||||
|         if (!result) { | ||||
|             CoreDomUtils.showErrorModal('Invalid data.'); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| @ -21,9 +21,6 @@ | ||||
| <!-- Identity providers. --> | ||||
| <ion-list *ngIf="identityProviders.length" class="core-login-identity-providers"> | ||||
|     <h2 class="item-heading">{{ 'core.login.potentialidps' | translate }}</h2> | ||||
|     <ion-button [fill]="'outline'" *ngFor="let provider of identityProviders" class="ion-text-wrap ion-margin core-oauth-provider" | ||||
|         (click)="oauthClicked(provider)" [ariaLabel]="provider.name" expand="block"> | ||||
|         <img *ngIf="provider.iconurl" [src]="provider.iconurl" alt="" width="32" height="32" slot="start" aria-hidden="true"> | ||||
|         <ion-label>{{ provider.name }}</ion-label> | ||||
|     </ion-button> | ||||
|     <core-identity-provider *ngFor="let provider of identityProviders" [provider]="provider" [launchurl]="siteConfig?.launchurl" | ||||
|         [redirectData]="redirectData" [siteUrl]="siteUrl" /> | ||||
| </ion-list> | ||||
|  | ||||
| @ -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<void>(); | ||||
| 
 | ||||
|     /** | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     async ngOnInit(): Promise<void> { | ||||
|         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<void> { | ||||
|         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<void> { | ||||
|         const result = await CoreLoginHelper.openBrowserForOAuthLogin( | ||||
|             this.siteUrl, | ||||
|             provider, | ||||
|             this.siteConfig?.launchurl, | ||||
|             this.redirectData, | ||||
|         ); | ||||
|     async extractCurrentLogin(): Promise<CoreLoginMethodsCurrentLogin | undefined> { | ||||
|         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; | ||||
| }; | ||||
|  | ||||
| @ -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.", | ||||
|  | ||||
| @ -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, | ||||
|  | ||||
| @ -51,18 +51,43 @@ | ||||
|             </div> | ||||
| 
 | ||||
|             <div class="core-login-methods"> | ||||
|                 <form *ngIf="showLoginForm && !isBrowserSSO" [formGroup]="credForm" (ngSubmit)="login($event)" class="core-login-form" | ||||
|                     #reconnectForm> | ||||
|                 @if (currentLogin && currentLogin.provider) { | ||||
|                 <core-identity-provider [provider]="currentLogin.provider" [launchurl]="siteConfig?.launchurl" [redirectData]="redirectData" | ||||
|                     [siteUrl]="site.siteUrl" /> | ||||
|                 @if (currentLogin.showOther) { | ||||
|                 <ion-accordion-group> | ||||
|                     <ion-accordion toggleIconSlot="start"> | ||||
|                         <ion-item class="ion-text-wrap" slot="header"> | ||||
|                             <ion-label> | ||||
|                                 <p class="item-heading">{{ 'core.login.morewaystologin' | translate }}</p> | ||||
|                             </ion-label> | ||||
|                         </ion-item> | ||||
| 
 | ||||
|                         <div slot="content"> | ||||
|                             <ng-template *ngTemplateOutlet="loginMethods" /> | ||||
|                         </div> | ||||
|                     </ion-accordion> | ||||
|                 </ion-accordion-group> | ||||
|                 } | ||||
|                 } @else { | ||||
|                 <ng-template *ngTemplateOutlet="loginMethods" /> | ||||
|                 } | ||||
|             </div> | ||||
|         </div> | ||||
|     </core-loading> | ||||
| </ion-content> | ||||
| 
 | ||||
| 
 | ||||
| <ng-template #loginMethods> | ||||
|     <form *ngIf="showLoginForm && !isBrowserSSO" [formGroup]="credForm" (ngSubmit)="login($event)" class="core-login-form" #reconnectForm> | ||||
|         <ion-item class="ion-margin-bottom" lines="inset"> | ||||
|                         <ion-input class="core-ioninput-password" name="password" type="password" | ||||
|                             placeholder="{{ 'core.login.password' | translate }}" formControlName="password" [clearOnEdit]="false" | ||||
|                             autocomplete="current-password" enterkeyhint="go" required="true" | ||||
|             <ion-input class="core-ioninput-password" name="password" type="password" placeholder="{{ 'core.login.password' | translate }}" | ||||
|                 formControlName="password" [clearOnEdit]="false" autocomplete="current-password" enterkeyhint="go" required="true" | ||||
|                 [attr.aria-label]="'core.login.password' | translate"> | ||||
|                 <ion-input-password-toggle slot="end" showIcon="fas-eye" hideIcon="fas-eye-slash" /> | ||||
|             </ion-input> | ||||
|         </ion-item> | ||||
|                     <ion-button type="submit" expand="block" [disabled]="!credForm.valid" | ||||
|                         class="ion-margin core-login-login-button ion-text-wrap"> | ||||
|         <ion-button type="submit" expand="block" [disabled]="!credForm.valid" class="ion-margin core-login-login-button ion-text-wrap"> | ||||
|             {{ 'core.login.loginbutton' | translate }} | ||||
|         </ion-button> | ||||
| 
 | ||||
| @ -74,8 +99,7 @@ | ||||
|     </form> | ||||
| 
 | ||||
|     <ng-container *ngIf="isBrowserSSO"> | ||||
|                     <ion-button expand="block" (click)="openBrowserSSO()" | ||||
|                         class="ion-margin core-login-login-inbrowser-button ion-text-wrap"> | ||||
|         <ion-button expand="block" (click)="openBrowserSSO()" class="ion-margin core-login-login-inbrowser-button ion-text-wrap"> | ||||
|             {{ 'core.login.loginbutton' | translate }} | ||||
|             <ion-icon name="fas-up-right-from-square" slot="end" aria-hidden="true" /> | ||||
|         </ion-button> | ||||
| @ -83,9 +107,6 @@ | ||||
|     </ng-container> | ||||
| 
 | ||||
|     <!-- Additional Login methods --> | ||||
|                 <core-login-methods *ngIf="siteConfig" [siteConfig]="siteConfig" [reconnect]="true" [siteUrl]="site.siteUrl" | ||||
|                     [redirectData]="redirectData" [showLoginForm]="showLoginForm" /> | ||||
|             </div> | ||||
|         </div> | ||||
|     </core-loading> | ||||
| </ion-content> | ||||
|     <core-login-methods *ngIf="siteConfig" [site]="site" [siteConfig]="siteConfig" [siteUrl]="site.siteUrl" [redirectData]="redirectData" | ||||
|         [showLoginForm]="showLoginForm" /> | ||||
| </ng-template> | ||||
|  | ||||
| @ -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(); | ||||
|  | ||||
| @ -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. | ||||
|  | ||||
| @ -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<void> { | ||||
|         const site = await this.getSite(siteId); | ||||
| 
 | ||||
|         site.setOAuthId(undefined); | ||||
| 
 | ||||
|         const newData: Partial<SiteDBEntry> = { | ||||
|             oauthId: null, | ||||
|         }; | ||||
| 
 | ||||
|         await this.sitesTable.update(newData, { id: siteId }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Updates a site's info. | ||||
|      * | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user