1
0
mirror of https://github.com/google/nomulus synced 2025-12-23 14:25:44 +00:00

Add OT&E UI to the new console (#2536)

This commit is contained in:
Pavlo Tkach
2024-08-23 16:53:45 -04:00
committed by GitHub
parent 0e808a4c01
commit 66513a114e
16 changed files with 340 additions and 4 deletions

View File

@@ -17,6 +17,9 @@ import { Route, RouterModule } from '@angular/router';
import { BillingInfoComponent } from './billingInfo/billingInfo.component'; import { BillingInfoComponent } from './billingInfo/billingInfo.component';
import { DomainListComponent } from './domains/domainList.component'; import { DomainListComponent } from './domains/domainList.component';
import { HomeComponent } from './home/home.component'; import { HomeComponent } from './home/home.component';
import { RegistryLockVerifyComponent } from './lock/registryLockVerify.component';
import { NewOteComponent } from './ote/newOte.component';
import { OteStatusComponent } from './ote/oteStatus.component';
import { RegistrarDetailsComponent } from './registrar/registrarDetails.component'; import { RegistrarDetailsComponent } from './registrar/registrarDetails.component';
import { RegistrarComponent } from './registrar/registrarsTable.component'; import { RegistrarComponent } from './registrar/registrarsTable.component';
import { ResourcesComponent } from './resources/resources.component'; import { ResourcesComponent } from './resources/resources.component';
@@ -26,7 +29,6 @@ import { SettingsComponent } from './settings/settings.component';
import UsersComponent from './settings/users/users.component'; import UsersComponent from './settings/users/users.component';
import WhoisComponent from './settings/whois/whois.component'; import WhoisComponent from './settings/whois/whois.component';
import { SupportComponent } from './support/support.component'; import { SupportComponent } from './support/support.component';
import { RegistryLockVerifyComponent } from './lock/registryLockVerify.component';
export interface RouteWithIcon extends Route { export interface RouteWithIcon extends Route {
iconName?: string; iconName?: string;
@@ -38,6 +40,14 @@ export const routes: RouteWithIcon[] = [
path: RegistryLockVerifyComponent.PATH, path: RegistryLockVerifyComponent.PATH,
component: RegistryLockVerifyComponent, component: RegistryLockVerifyComponent,
}, },
{
path: NewOteComponent.PATH,
component: NewOteComponent,
},
{
path: OteStatusComponent.PATH,
component: OteStatusComponent,
},
{ path: 'registrars', component: RegistrarComponent }, { path: 'registrars', component: RegistrarComponent },
{ {
path: 'home', path: 'home',

View File

@@ -30,7 +30,10 @@ import { DomainListComponent } from './domains/domainList.component';
import { RegistryLockComponent } from './domains/registryLock.component'; import { RegistryLockComponent } from './domains/registryLock.component';
import { HeaderComponent } from './header/header.component'; import { HeaderComponent } from './header/header.component';
import { HomeComponent } from './home/home.component'; import { HomeComponent } from './home/home.component';
import { RegistryLockVerifyComponent } from './lock/registryLockVerify.component';
import { NavigationComponent } from './navigation/navigation.component'; import { NavigationComponent } from './navigation/navigation.component';
import { NewOteComponent } from './ote/newOte.component';
import { OteStatusComponent } from './ote/oteStatus.component';
import NewRegistrarComponent from './registrar/newRegistrar.component'; import NewRegistrarComponent from './registrar/newRegistrar.component';
import { RegistrarDetailsComponent } from './registrar/registrarDetails.component'; import { RegistrarDetailsComponent } from './registrar/registrarDetails.component';
import { RegistrarSelectorComponent } from './registrar/registrarSelector.component'; import { RegistrarSelectorComponent } from './registrar/registrarSelector.component';
@@ -54,7 +57,6 @@ import { UserDataService } from './shared/services/userData.service';
import { SnackBarModule } from './snackbar.module'; import { SnackBarModule } from './snackbar.module';
import { SupportComponent } from './support/support.component'; import { SupportComponent } from './support/support.component';
import { TldsComponent } from './tlds/tlds.component'; import { TldsComponent } from './tlds/tlds.component';
import { RegistryLockVerifyComponent } from './lock/registryLockVerify.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@@ -66,6 +68,8 @@ import { RegistryLockVerifyComponent } from './lock/registryLockVerify.component
HeaderComponent, HeaderComponent,
HomeComponent, HomeComponent,
LocationBackDirective, LocationBackDirective,
NewOteComponent,
OteStatusComponent,
UserLevelVisibility, UserLevelVisibility,
NavigationComponent, NavigationComponent,
NewRegistrarComponent, NewRegistrarComponent,

View File

@@ -0,0 +1,36 @@
<h1 class="mat-headline-4">Generate OT&E Accounts</h1>
<div class="console-app__new-ote">
@if (oteCreateResponseFormatted()) {
<h1>Generated Successfully</h1>
<mat-card appearance="outlined">
<mat-card-header>
<mat-card-title>Epp Credentials</mat-card-title>
<mat-card-subtitle
>Copy and paste this into an email to the registrars</mat-card-subtitle
>
</mat-card-header>
<mat-card-content>
<p>{{ oteCreateResponseFormatted() }}</p>
</mat-card-content>
</mat-card>
} @else {
<form (ngSubmit)="onSubmit()" [formGroup]="createOte">
<p>
<mat-form-field name="registrarId" appearance="outline">
<mat-label>Base Registrar Id: </mat-label>
<input matInput type="text" formControlName="registrarId" required />
</mat-form-field>
</p>
<p>
<mat-form-field name="registrarEmail" appearance="outline">
<mat-label>Contact Email: </mat-label>
<input matInput type="text" formControlName="registrarEmail" required />
<mat-hint
>Will be granted web-console access to the OTE registrars.</mat-hint
>
</mat-form-field>
</p>
<button mat-flat-button color="primary" type="submit">Save</button>
</form>
}
</div>

View File

@@ -0,0 +1,7 @@
.console-app__new-ote {
max-width: 720px;
mat-card-content {
white-space: break-spaces;
padding: 20px;
}
}

View File

@@ -0,0 +1,81 @@
// 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, signal } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { MatSnackBar } from '@angular/material/snack-bar';
import { RegistrarService } from '../registrar/registrar.service';
export interface OteCreateResponse extends Map<string, string> {
password: string;
}
@Component({
selector: 'app-ote',
templateUrl: './newOte.component.html',
styleUrls: ['./newOte.component.scss'],
})
export class NewOteComponent {
public static PATH = 'new-ote';
oteCreateResponse = signal<OteCreateResponse | undefined>(undefined);
readonly oteCreateResponseFormatted = computed(() => {
const oteCreateResponse = this.oteCreateResponse();
if (oteCreateResponse) {
const { password } = oteCreateResponse;
return Object.entries(oteCreateResponse)
.filter((entry) => entry[0] !== 'password')
.map(
([login, tld]) =>
`Login: ${login}\t\tPassword: ${password}\t\tTLD: ${tld}`
)
.join('\n');
}
return undefined;
});
createOte = new FormGroup({
registrarId: new FormControl('', [Validators.required]),
registrarEmail: new FormControl('', [Validators.required]),
});
constructor(
protected registrarService: RegistrarService,
private _snackBar: MatSnackBar
) {}
onSubmit() {
if (this.createOte.valid) {
const { registrarId, registrarEmail } = this.createOte.value;
this.registrarService
.generateOte(
{
registrarId,
registrarEmail,
},
registrarId || ''
)
.subscribe({
next: (oteCreateResponse: OteCreateResponse) => {
this.oteCreateResponse.set(oteCreateResponse);
},
error: (err: HttpErrorResponse) => {
this._snackBar.open(err.error || err.message);
},
});
}
}
}

View File

@@ -0,0 +1,31 @@
<app-selected-registrar-wrapper>
<h1 class="mat-headline-4">OT&E Status Check</h1>
@if(isOte()) {
<h1 *ngIf="oteStatusResponse().length">
Status:
<span>{{ oteStatusUnfinished().length ? "Unfinished" : "Completed" }}</span>
</h1>
<div class="console-app__ote-status">
@if(oteStatusCompleted().length) {
<div class="console-app__ote-status_completed">
<h1>Completed</h1>
<div *ngFor="let entry of oteStatusCompleted()">
<mat-icon>check_box</mat-icon>{{ entry.description }}
</div>
</div>
} @if(oteStatusUnfinished().length) {
<div class="console-app__ote-status_unfinished">
<h1>Unfinished</h1>
<div *ngFor="let entry of oteStatusUnfinished()">
<mat-icon>check_box_outline_blank</mat-icon>{{ entry.description }}
</div>
</div>
}
</div>
} @else {
<h1>
Registrar {{ registrarService.registrar()?.registrarId }} is not an OT&E
registrar
</h1>
}
</app-selected-registrar-wrapper>

View File

@@ -0,0 +1,28 @@
.console-app__ote-status {
max-width: 730px;
display: flex;
flex-wrap: wrap;
&_completed,
&_unfinished {
border: 1px solid #ddd;
padding: 20px;
border-radius: 10px;
margin: 0 20px 30px 0;
div {
display: flex;
min-width: 300px;
align-items: flex-start;
max-width: 300px;
margin-bottom: 10px;
padding-bottom: 5px;
border-bottom: 1px solid #ddd;
&:last-child {
border: none;
}
}
mat-icon {
min-width: 30px;
}
}
}

View File

@@ -0,0 +1,62 @@
// 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, signal } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import { RegistrarService } from '../registrar/registrar.service';
export interface OteStatusResponse {
description: string;
requirement: number;
timesPerformed: number;
completed: boolean;
}
@Component({
selector: 'app-ote-status',
templateUrl: './oteStatus.component.html',
styleUrls: ['./oteStatus.component.scss'],
})
export class OteStatusComponent {
public static PATH = 'ote-status';
oteStatusResponse = signal<OteStatusResponse[]>([]);
oteStatusCompleted = computed(() =>
this.oteStatusResponse().filter((v) => v.completed)
);
oteStatusUnfinished = computed(() =>
this.oteStatusResponse().filter((v) => !v.completed)
);
isOte = computed(
() => this.registrarService.registrar()?.type?.toLowerCase() === 'ote'
);
constructor(
protected registrarService: RegistrarService,
private _snackBar: MatSnackBar
) {
this.registrarService
.oteStatus(this.registrarService.registrarId())
.subscribe({
next: (oteStatusResponse: OteStatusResponse[]) => {
this.oteStatusResponse.set(oteStatusResponse);
},
error: (err: HttpErrorResponse) => {
this._snackBar.open(err.error || err.message);
},
});
}
}

View File

@@ -17,6 +17,8 @@ import { Observable, switchMap, tap } from 'rxjs';
import { MatSnackBar } from '@angular/material/snack-bar'; import { MatSnackBar } from '@angular/material/snack-bar';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { OteCreateResponse } from '../ote/newOte.component';
import { OteStatusResponse } from '../ote/oteStatus.component';
import { BackendService } from '../shared/services/backend.service'; import { BackendService } from '../shared/services/backend.service';
import { import {
GlobalLoader, GlobalLoader,
@@ -69,6 +71,7 @@ export interface Registrar
registrarId: string; registrarId: string;
registrarName: string; registrarName: string;
registryLockAllowed?: boolean; registryLockAllowed?: boolean;
type?: string;
} }
@Injectable({ @Injectable({
@@ -149,4 +152,17 @@ export class RegistrarService implements GlobalLoader {
loadingTimeout() { loadingTimeout() {
this._snackBar.open('Timeout loading registrars'); this._snackBar.open('Timeout loading registrars');
} }
generateOte(
oteForm: Object,
registrarId: string
): Observable<OteCreateResponse> {
return this.backend
.generateOte(oteForm, registrarId)
.pipe(tap((_) => this.loadRegistrars()));
}
oteStatus(registrarId: string): Observable<OteStatusResponse[]> {
return this.backend.getOteStatus(registrarId);
}
} }

View File

@@ -8,6 +8,14 @@
</button> </button>
<div class="spacer"></div> <div class="spacer"></div>
@if(!inEdit && !registrarNotFound) { @if(!inEdit && !registrarNotFound) {
<button
mat-stroked-button
(click)="checkOteStatus()"
aria-label="Check OT&E account"
[elementId]="getElementIdForOteBlock()"
>
Check OT&E Status
</button>
<button <button
mat-flat-button mat-flat-button
color="primary" color="primary"

View File

@@ -18,6 +18,8 @@ import { MatChipInputEvent } from '@angular/material/chips';
import { MatSnackBar } from '@angular/material/snack-bar'; import { MatSnackBar } from '@angular/material/snack-bar';
import { ActivatedRoute, ParamMap, Router } from '@angular/router'; import { ActivatedRoute, ParamMap, Router } from '@angular/router';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { OteStatusComponent } from '../ote/oteStatus.component';
import { RESTRICTED_ELEMENTS } from '../shared/directives/userLevelVisiblity.directive';
import { Registrar, RegistrarService } from './registrar.service'; import { Registrar, RegistrarService } from './registrar.service';
import { RegistrarComponent, columns } from './registrarsTable.component'; import { RegistrarComponent, columns } from './registrarsTable.component';
@@ -70,6 +72,14 @@ export class RegistrarDetailsComponent implements OnInit {
]; ];
} }
checkOteStatus() {
this.router.navigate([OteStatusComponent.PATH]);
}
getElementIdForOteBlock() {
return RESTRICTED_ELEMENTS.OTE;
}
removeTLD(tld: string) { removeTLD(tld: string) {
this.registrarInEdit.allowedTlds = this.registrarInEdit.allowedTlds?.filter( this.registrarInEdit.allowedTlds = this.registrarInEdit.allowedTlds?.filter(
(v) => v != tld (v) => v != tld
@@ -91,6 +101,6 @@ export class RegistrarDetailsComponent implements OnInit {
} }
ngOnDestroy() { ngOnDestroy() {
this.subscription.unsubscribe(); this.subscription && this.subscription.unsubscribe();
} }
} }

View File

@@ -4,7 +4,17 @@
<div class="console-app__registrars"> <div class="console-app__registrars">
<div class="console-app__registrars-header"> <div class="console-app__registrars-header">
<h1 class="mat-headline-4">Registrars</h1> <h1 class="mat-headline-4">Registrars</h1>
<div class="spacer"></div>
<button <button
mat-stroked-button
(click)="createOteAccount()"
aria-label="Generate OT&E accounts"
[elementId]="getElementIdForOteBlock()"
>
Create OT&E accounts
</button>
<button
class="console-app__registrars-new"
mat-flat-button mat-flat-button
color="primary" color="primary"
(click)="openNewRegistrar()" (click)="openNewRegistrar()"

View File

@@ -10,6 +10,10 @@
min-width: $min-width !important; min-width: $min-width !important;
} }
&__registrars-new {
margin-left: 20px;
}
&__registrars-header { &__registrars-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;

View File

@@ -17,6 +17,8 @@ import { MatPaginator } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort'; import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table'; import { MatTableDataSource } from '@angular/material/table';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { NewOteComponent } from '../ote/newOte.component';
import { RESTRICTED_ELEMENTS } from '../shared/directives/userLevelVisiblity.directive';
import { Registrar, RegistrarService } from './registrar.service'; import { Registrar, RegistrarService } from './registrar.service';
export const columns = [ export const columns = [
@@ -103,6 +105,14 @@ export class RegistrarComponent {
this.dataSource.sort = this.sort; this.dataSource.sort = this.sort;
} }
createOteAccount() {
this.router.navigate([NewOteComponent.PATH]);
}
getElementIdForOteBlock() {
return RESTRICTED_ELEMENTS.OTE;
}
openDetails(registrarId: string) { openDetails(registrarId: string) {
this.router.navigate(['registrars/', registrarId], { this.router.navigate(['registrars/', registrarId], {
queryParamsHandling: 'merge', queryParamsHandling: 'merge',

View File

@@ -17,10 +17,11 @@ import { UserDataService } from '../services/userData.service';
export enum RESTRICTED_ELEMENTS { export enum RESTRICTED_ELEMENTS {
REGISTRAR_ELEMENT, REGISTRAR_ELEMENT,
OTE,
} }
export const DISABLED_ELEMENTS_PER_ROLE = { export const DISABLED_ELEMENTS_PER_ROLE = {
NONE: [RESTRICTED_ELEMENTS.REGISTRAR_ELEMENT], NONE: [RESTRICTED_ELEMENTS.REGISTRAR_ELEMENT, RESTRICTED_ELEMENTS.OTE],
}; };
@Directive({ @Directive({

View File

@@ -19,6 +19,8 @@ import { Observable, catchError, of, throwError } from 'rxjs';
import { DomainListResult } from 'src/app/domains/domainList.service'; import { DomainListResult } from 'src/app/domains/domainList.service';
import { DomainLocksResult } from 'src/app/domains/registryLock.service'; import { DomainLocksResult } from 'src/app/domains/registryLock.service';
import { RegistryLockVerificationResponse } from 'src/app/lock/registryLockVerify.service'; import { RegistryLockVerificationResponse } from 'src/app/lock/registryLockVerify.service';
import { OteCreateResponse } from 'src/app/ote/newOte.component';
import { OteStatusResponse } from 'src/app/ote/oteStatus.component';
import { import {
Registrar, Registrar,
SecuritySettingsBackendModel, SecuritySettingsBackendModel,
@@ -198,6 +200,22 @@ export class BackendService {
.pipe(catchError((err) => this.errorCatcher<DomainLocksResult[]>(err))); .pipe(catchError((err) => this.errorCatcher<DomainLocksResult[]>(err)));
} }
generateOte(
oteForm: Object,
registrarId: string
): Observable<OteCreateResponse> {
return this.http.post<OteCreateResponse>(
`/console-api/ote?registrarId=${registrarId}`,
oteForm
);
}
getOteStatus(registrarId: string) {
return this.http
.get<OteStatusResponse[]>(`/console-api/ote?registrarId=${registrarId}`)
.pipe(catchError((err) => this.errorCatcher<OteStatusResponse[]>(err)));
}
verifyRegistryLockRequest( verifyRegistryLockRequest(
lockVerificationCode: string lockVerificationCode: string
): Observable<RegistryLockVerificationResponse> { ): Observable<RegistryLockVerificationResponse> {