From 213e06f02ea27662b9a562ff874dc1a8a414ccab Mon Sep 17 00:00:00 2001 From: Pavlo Tkach <3469726+ptkach@users.noreply.github.com> Date: Fri, 26 Jul 2024 12:02:19 -0400 Subject: [PATCH] Add registry lock ui (#2500) --- console-webapp/src/app/app.module.ts | 2 + .../src/app/domains/domainList.component.html | 49 +++++++++- .../src/app/domains/domainList.component.scss | 6 ++ .../src/app/domains/domainList.component.ts | 32 ++++++- .../src/app/domains/domainList.service.ts | 10 +- .../app/domains/registryLock.component.html | 81 ++++++++++++++++ .../app/domains/registryLock.component.scss | 20 ++++ .../src/app/domains/registryLock.component.ts | 92 +++++++++++++++++++ .../src/app/domains/registryLock.service.ts | 59 ++++++++++++ .../src/app/home/home.component.html | 5 +- console-webapp/src/app/home/home.component.ts | 4 + .../app/resources/resources.component.html | 2 +- .../userLevelVisiblity.directive.ts | 15 ++- .../app/shared/services/backend.service.ts | 29 +++++- .../app/shared/services/userData.service.ts | 6 +- .../src/app/support/support.component.html | 16 ++-- 16 files changed, 398 insertions(+), 30 deletions(-) create mode 100644 console-webapp/src/app/domains/registryLock.component.html create mode 100644 console-webapp/src/app/domains/registryLock.component.scss create mode 100644 console-webapp/src/app/domains/registryLock.component.ts create mode 100644 console-webapp/src/app/domains/registryLock.service.ts diff --git a/console-webapp/src/app/app.module.ts b/console-webapp/src/app/app.module.ts index 40ddeeee7..8f41f6217 100644 --- a/console-webapp/src/app/app.module.ts +++ b/console-webapp/src/app/app.module.ts @@ -27,6 +27,7 @@ import { provideHttpClient } from '@angular/common/http'; import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field'; import { BillingInfoComponent } from './billingInfo/billingInfo.component'; import { DomainListComponent } from './domains/domainList.component'; +import { RegistryLockComponent } from './domains/registryLock.component'; import { HeaderComponent } from './header/header.component'; import { HomeComponent } from './home/home.component'; import { NavigationComponent } from './navigation/navigation.component'; @@ -71,6 +72,7 @@ import { RegistryLockVerifyComponent } from './lock/registryLockVerify.component NotificationsComponent, RegistrarComponent, RegistrarDetailsComponent, + RegistryLockComponent, RegistrarSelectorComponent, RegistryLockVerifyComponent, ResourcesComponent, diff --git a/console-webapp/src/app/domains/domainList.component.html b/console-webapp/src/app/domains/domainList.component.html index 0020a123d..8fd74db32 100644 --- a/console-webapp/src/app/domains/domainList.component.html +++ b/console-webapp/src/app/domains/domainList.component.html @@ -2,6 +2,17 @@

Domains

+
+ + +
+ @if (!isLoading && totalResults == 0) {

@@ -12,7 +23,18 @@

No domains found

} @else { -
+ + + + + +
@if (isLoading) { @@ -78,6 +100,29 @@ }} + + Registry-Locked + {{ + isDomainLocked(element.domainName) + }} + + + + Actions + + + + + @@ -85,7 +130,7 @@ - No domains found + No domains found = new MatTableDataSource(); @@ -52,15 +54,16 @@ export class DomainListComponent { @ViewChild(MatPaginator, { static: true }) paginator!: MatPaginator; constructor( - private backendService: BackendService, - private domainListService: DomainListService, + protected domainListService: DomainListService, protected registrarService: RegistrarService, + protected registryLockService: RegistryLockService, private _snackBar: MatSnackBar ) { effect(() => { this.pageNumber = 0; this.totalResults = 0; if (this.registrarService.registrarId()) { + this.loadLocks(); this.reloadData(); } }); @@ -80,6 +83,25 @@ export class DomainListComponent { this.searchTermSubject.complete(); } + openRegistryLock(domainName: string) { + this.domainListService.selectedDomain = domainName; + this.domainListService.activeActionComponent = RegistryLockComponent; + } + + loadLocks() { + this.registryLockService.retrieveLocks().subscribe({ + error: (err: HttpErrorResponse) => { + this._snackBar.open(err.message); + }, + }); + } + + isDomainLocked(domainName: string) { + return this.registryLockService.domainsLocks.some( + (d) => d.domainName === domainName + ); + } + reloadData() { this.isLoading = true; this.domainListService @@ -95,7 +117,7 @@ export class DomainListComponent { this.isLoading = false; }, next: (domainListResult) => { - this.dataSource.data = (domainListResult || {}).domains; + this.dataSource.data = this.domainListService.domainsList; this.totalResults = (domainListResult || {}).totalResults || 0; this.isLoading = false; }, diff --git a/console-webapp/src/app/domains/domainList.service.ts b/console-webapp/src/app/domains/domainList.service.ts index 93651a3a7..3a73cf716 100644 --- a/console-webapp/src/app/domains/domainList.service.ts +++ b/console-webapp/src/app/domains/domainList.service.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Injectable } from '@angular/core'; +import { Injectable, Type } from '@angular/core'; import { tap } from 'rxjs'; import { RegistrarService } from '../registrar/registrar.service'; import { BackendService } from '../shared/services/backend.service'; @@ -35,9 +35,14 @@ export interface DomainListResult { totalResults: number; } -@Injectable() +@Injectable({ + providedIn: 'root', +}) export class DomainListService { checkpointTime?: string; + selectedDomain?: string; + public activeActionComponent: Type | null = null; + public domainsList: Domain[] = []; constructor( private backendService: BackendService, @@ -62,6 +67,7 @@ export class DomainListService { .pipe( tap((domainListResult: DomainListResult) => { this.checkpointTime = domainListResult?.checkpointTime; + this.domainsList = domainListResult?.domains; }) ); } diff --git a/console-webapp/src/app/domains/registryLock.component.html b/console-webapp/src/app/domains/registryLock.component.html new file mode 100644 index 000000000..6d20416c1 --- /dev/null +++ b/console-webapp/src/app/domains/registryLock.component.html @@ -0,0 +1,81 @@ +
+

+ +

+ + @if(!registrarService.registrar()?.registryLockAllowed) { +

+ Sorry, your registrar hasn't enrolled in registry lock yet. To do so, please + contact {{ userDataService.userData()?.supportEmail }}. +

+ } @else if (isLocked()) { +

Unlock the domain {{ domainListService.selectedDomain }}

+
+

+ Password: + + + +

+

+ Automatically re-lock the domain after: + + @for (option of relockOptions; track option.name) { + {{ + option.name + }} + } + +

+ +
+ priority_highConfirmation email will be sent to your + email address to confirm the unlock +
+ +
+ } @else { +

Lock the domain {{ domainListService.selectedDomain }}

+
+

+ Password: + + + +

+ +
+ priority_highThe lock will not take effect until you + click the confirmation link that will be emailed to you. When it takes + effect, you will be billed the standard server status change billing cost. +
+ +
+ } +
diff --git a/console-webapp/src/app/domains/registryLock.component.scss b/console-webapp/src/app/domains/registryLock.component.scss new file mode 100644 index 000000000..a573b3bef --- /dev/null +++ b/console-webapp/src/app/domains/registryLock.component.scss @@ -0,0 +1,20 @@ +.console-app { + &__registry-lock { + mat-label { + display: block; + margin-bottom: 10px; + } + p { + margin-bottom: 40px; + } + } + &__registry-lock-notification { + padding: 20px; + border-radius: 10px; + background-color: var(--light-highlight); + margin-bottom: 20px; + width: max-content; + display: flex; + align-items: center; + } +} diff --git a/console-webapp/src/app/domains/registryLock.component.ts b/console-webapp/src/app/domains/registryLock.component.ts new file mode 100644 index 000000000..f0f120395 --- /dev/null +++ b/console-webapp/src/app/domains/registryLock.component.ts @@ -0,0 +1,92 @@ +// Copyright 2024 The Nomulus Authors. All Rights Reserved. +// +// 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 { HttpErrorResponse } from '@angular/common/http'; +import { Component, computed } from '@angular/core'; +import { FormControl, FormGroup } from '@angular/forms'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { RegistrarService } from '../registrar/registrar.service'; +import { UserDataService } from '../shared/services/userData.service'; +import { DomainListService } from './domainList.service'; +import { RegistryLockService } from './registryLock.service'; + +@Component({ + selector: 'app-registry-lock', + templateUrl: './registryLock.component.html', + styleUrls: ['./registryLock.component.scss'], +}) +export class RegistryLockComponent { + readonly isLocked = computed(() => + this.registryLockService.domainsLocks.some( + (dl) => dl.domainName === this.domainListService.selectedDomain + ) + ); + + relockOptions = [ + { name: '1 hour', duration: 3600000 }, + { name: '6 hours', duration: 21600000 }, + { name: '24 hours', duration: 86400000 }, + { name: 'Never', duration: undefined }, + ]; + + lockDomain = new FormGroup({ + password: new FormControl(''), + }); + + unlockDomain = new FormGroup({ + password: new FormControl(''), + relockTime: new FormControl(undefined), + }); + + constructor( + protected registrarService: RegistrarService, + protected domainListService: DomainListService, + protected registryLockService: RegistryLockService, + protected userDataService: UserDataService, + private _snackBar: MatSnackBar + ) {} + + goBack() { + this.domainListService.selectedDomain = undefined; + this.domainListService.activeActionComponent = null; + } + + save(isLock: boolean) { + let request; + if (!isLock) { + request = this.registryLockService.registryLockDomain( + this.domainListService.selectedDomain || '', + this.unlockDomain.value.password || '', + this.unlockDomain.value.relockTime || undefined, + isLock + ); + } else { + request = this.registryLockService.registryLockDomain( + this.domainListService.selectedDomain || '', + this.lockDomain.value.password || '', + undefined, + isLock + ); + } + + request.subscribe({ + complete: () => { + this.goBack(); + }, + error: (err: HttpErrorResponse) => { + this._snackBar.open(err.error); + }, + }); + } +} diff --git a/console-webapp/src/app/domains/registryLock.service.ts b/console-webapp/src/app/domains/registryLock.service.ts new file mode 100644 index 000000000..e4b87707c --- /dev/null +++ b/console-webapp/src/app/domains/registryLock.service.ts @@ -0,0 +1,59 @@ +// Copyright 2024 The Nomulus Authors. All Rights Reserved. +// +// 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 { tap } from 'rxjs'; +import { RegistrarService } from '../registrar/registrar.service'; +import { BackendService } from '../shared/services/backend.service'; + +export interface DomainLocksResult { + domainName: string; +} + +@Injectable({ + providedIn: 'root', +}) +export class RegistryLockService { + public domainsLocks: DomainLocksResult[] = []; + + constructor( + private backendService: BackendService, + private registrarService: RegistrarService + ) {} + + retrieveLocks() { + return this.backendService + .getLocks(this.registrarService.registrarId()) + .pipe( + tap((domainLocksResult) => { + this.domainsLocks = domainLocksResult; + }) + ); + } + + registryLockDomain( + domainName: string, + password: string, + relockDurationMillis: number | undefined, + isLock: boolean + ) { + return this.backendService.registryLockDomain( + domainName, + password, + relockDurationMillis, + this.registrarService.registrarId(), + isLock + ); + } +} diff --git a/console-webapp/src/app/home/home.component.html b/console-webapp/src/app/home/home.component.html index 148f3b820..c41a41b15 100644 --- a/console-webapp/src/app/home/home.component.html +++ b/console-webapp/src/app/home/home.component.html @@ -34,7 +34,10 @@ - +

account_circle diff --git a/console-webapp/src/app/home/home.component.ts b/console-webapp/src/app/home/home.component.ts index 5b21cc8a0..481a13c0b 100644 --- a/console-webapp/src/app/home/home.component.ts +++ b/console-webapp/src/app/home/home.component.ts @@ -18,6 +18,7 @@ import { DomainListComponent } from '../domains/domainList.component'; import { RegistrarComponent } from '../registrar/registrarsTable.component'; import SecurityComponent from '../settings/security/security.component'; import { SettingsComponent } from '../settings/settings.component'; +import { RESTRICTED_ELEMENTS } from '../shared/directives/userLevelVisiblity.directive'; import { BreakPointObserverService } from '../shared/services/breakPoint.service'; @Component({ @@ -30,6 +31,9 @@ export class HomeComponent { protected breakPointObserverService: BreakPointObserverService, private router: Router ) {} + getElementIdForRegistrarsBlock() { + return RESTRICTED_ELEMENTS.REGISTRAR_ELEMENT; + } viewRegistrars() { this.router.navigate([RegistrarComponent.PATH], { queryParamsHandling: 'merge', diff --git a/console-webapp/src/app/resources/resources.component.html b/console-webapp/src/app/resources/resources.component.html index c828e361d..077eba0ef 100644 --- a/console-webapp/src/app/resources/resources.component.html +++ b/console-webapp/src/app/resources/resources.component.html @@ -4,7 +4,7 @@
Technical resources
View onboarding FAQs, TLD information, and technical documentation on Google Drive { + return this.http + .get( + `/console-api/registry-lock?registrarId=${registrarId}` + ) + .pipe(catchError((err) => this.errorCatcher(err))); + } + verifyRegistryLockRequest( lockVerificationCode: string ): Observable { diff --git a/console-webapp/src/app/shared/services/userData.service.ts b/console-webapp/src/app/shared/services/userData.service.ts index 73d7d562d..f74924189 100644 --- a/console-webapp/src/app/shared/services/userData.service.ts +++ b/console-webapp/src/app/shared/services/userData.service.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Injectable } from '@angular/core'; +import { Injectable, signal } from '@angular/core'; import { MatSnackBar } from '@angular/material/snack-bar'; import { Observable, tap } from 'rxjs'; import { BackendService } from './backend.service'; @@ -33,7 +33,7 @@ export interface UserData { providedIn: 'root', }) export class UserDataService implements GlobalLoader { - public userData!: UserData; + userData = signal(undefined); constructor( private backend: BackendService, protected globalLoader: GlobalLoaderService, @@ -48,7 +48,7 @@ export class UserDataService implements GlobalLoader { getUserData(): Observable { return this.backend.getUserData().pipe( tap((userData: UserData) => { - this.userData = userData; + this.userData.set(userData); }) ); } diff --git a/console-webapp/src/app/support/support.component.html b/console-webapp/src/app/support/support.component.html index f25513a35..ccab7d769 100644 --- a/console-webapp/src/app/support/support.component.html +++ b/console-webapp/src/app/support/support.component.html @@ -17,9 +17,11 @@ For general purpose questions once you are integrated with our registry system. If the issue is urgent, please put "Urgent" in the email title.

- {{ - userDataService.userData.supportEmail - }} + {{ userDataService.userData()?.supportEmail }}

Note: You may receive occasional service announcements via registrar-announcement@google.com. You will not be able to reply to @@ -29,13 +31,13 @@

For general support inquiries 24/7:

{{ userDataService.userData.supportPhoneNumber }}{{ userDataService.userData()?.supportPhoneNumber }} - @if (userDataService.userData.passcode) { + @if (userDataService.userData()?.passcode) {

Your telephone passcode:

- {{ userDataService.userData.passcode }} + {{ userDataService.userData()?.passcode }}

Note: Please be ready with your account name and telephone passcode when