mirror of
https://github.com/google/nomulus
synced 2026-09-16 13:04:19 +00:00
Restyle registrar console based on the new design proposal (#2336)
This commit is contained in:
@@ -1,48 +1,40 @@
|
||||
@if (loading) {
|
||||
<div class="contact__loading">
|
||||
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
|
||||
</div>
|
||||
@if(contactService.isContactDetailsView || contactService.isContactNewView) {
|
||||
<app-contact-details></app-contact-details>
|
||||
} @else {
|
||||
<div>
|
||||
<div class="console-app__contacts-controls">
|
||||
<button
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
(click)="openNewContact()"
|
||||
aria-label="Add new contact"
|
||||
>
|
||||
<mat-icon>add</mat-icon>
|
||||
Add new contact
|
||||
</button>
|
||||
</div>
|
||||
<div class="console-app__contacts">
|
||||
@if (contactService.contacts().length === 0) {
|
||||
<div class="contact__empty-contacts">
|
||||
<mat-icon class="contact__empty-contacts-icon secondary-text"
|
||||
<div class="console-app__empty-contacts">
|
||||
<mat-icon class="console-app__empty-contacts-icon secondary-text"
|
||||
>apps_outage</mat-icon
|
||||
>
|
||||
<h1>No contacts found</h1>
|
||||
</div>
|
||||
} @else { @for (group of groupedContacts(); track group.emailAddress) {
|
||||
<div class="contact__cards-wrapper">
|
||||
<h3>{{ group.label }}s</h3>
|
||||
<mat-divider></mat-divider>
|
||||
<div class="contact__cards">
|
||||
<mat-card class="contact__card" *ngFor="let contact of group.contacts">
|
||||
<mat-card-title>{{ contact.name }}</mat-card-title>
|
||||
<p *ngIf="contact.phoneNumber">{{ contact.phoneNumber }}</p>
|
||||
<p *ngIf="contact.emailAddress">{{ contact.emailAddress }}</p>
|
||||
<mat-card-actions class="contact__card-actions">
|
||||
<button
|
||||
mat-button
|
||||
color="primary"
|
||||
(click)="openDetails($event, contact)"
|
||||
>
|
||||
<mat-icon>edit</mat-icon>Edit
|
||||
</button>
|
||||
<button mat-button color="accent" (click)="deleteContact(contact)">
|
||||
<mat-icon>delete</mat-icon>Delete
|
||||
</button>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
</div>
|
||||
</div>
|
||||
} }
|
||||
<div class="contact__actions">
|
||||
<button mat-raised-button color="primary" (click)="openCreateNew($event)">
|
||||
<mat-icon>add</mat-icon>Create a Contact
|
||||
</button>
|
||||
</div>
|
||||
<app-dialog-bottom-sheet-wrapper
|
||||
#contactDetailsWrapper
|
||||
></app-dialog-bottom-sheet-wrapper>
|
||||
} @else {
|
||||
<mat-table [dataSource]="dataSource" class="mat-elevation-z0">
|
||||
<ng-container
|
||||
*ngFor="let column of columns"
|
||||
[matColumnDef]="column.columnDef"
|
||||
>
|
||||
<mat-header-cell *matHeaderCellDef> {{ column.header }} </mat-header-cell>
|
||||
<mat-cell *matCellDef="let row" [innerHTML]="column.cell(row)"></mat-cell>
|
||||
</ng-container>
|
||||
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
|
||||
<mat-row
|
||||
*matRowDef="let row; columns: displayedColumns"
|
||||
(click)="openDetails(row)"
|
||||
></mat-row>
|
||||
</mat-table>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
||||
// 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.
|
||||
@@ -12,30 +12,14 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
.contact {
|
||||
&__cards-wrapper {
|
||||
margin-top: 22px;
|
||||
}
|
||||
&__card {
|
||||
width: 400px;
|
||||
padding: 15px;
|
||||
}
|
||||
&__card-actions {
|
||||
padding: 0;
|
||||
}
|
||||
&__loading {
|
||||
margin: 2rem 0;
|
||||
}
|
||||
&__cards {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
&__actions {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
margin: 2rem 0;
|
||||
.console-app {
|
||||
&__contacts {
|
||||
margin-top: -10px;
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
mat-table {
|
||||
min-width: 800px;
|
||||
}
|
||||
}
|
||||
&__empty-contacts {
|
||||
display: flex;
|
||||
@@ -49,22 +33,14 @@
|
||||
font-size: 4rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
}
|
||||
.contact-details {
|
||||
&__input {
|
||||
width: 100%;
|
||||
&__contacts-controls {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
top: 5px;
|
||||
}
|
||||
&__group {
|
||||
margin: 20px 0;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
}
|
||||
&__group-content {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
max-width: 450px;
|
||||
* {
|
||||
min-width: 200px;
|
||||
}
|
||||
.contact__name-column-title {
|
||||
color: #5f6368;
|
||||
font-weight: 500;
|
||||
padding: 10px 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
||||
// 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.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
||||
// 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.
|
||||
@@ -12,101 +12,16 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, ViewChild, computed } from '@angular/core';
|
||||
import { Contact, ContactService } from './contact.service';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { Component, effect } from '@angular/core';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { take } from 'rxjs';
|
||||
import { RegistrarService } from 'src/app/registrar/registrar.service';
|
||||
import {
|
||||
DialogBottomSheetContent,
|
||||
DialogBottomSheetWrapper,
|
||||
} from 'src/app/shared/components/dialogBottomSheet.component';
|
||||
|
||||
enum Operations {
|
||||
DELETE,
|
||||
ADD,
|
||||
UPDATE,
|
||||
}
|
||||
|
||||
interface GroupedContacts {
|
||||
value: string;
|
||||
label: string;
|
||||
contacts: Array<Contact>;
|
||||
}
|
||||
|
||||
type ContactDetailsParams = {
|
||||
close: Function;
|
||||
data: {
|
||||
contact: Contact;
|
||||
operation: Operations;
|
||||
};
|
||||
};
|
||||
|
||||
const contactTypes: Array<GroupedContacts> = [
|
||||
{ value: 'ADMIN', label: 'Primary contact', contacts: [] },
|
||||
{ value: 'ABUSE', label: 'Abuse contact', contacts: [] },
|
||||
{ value: 'BILLING', label: 'Billing contact', contacts: [] },
|
||||
{ value: 'LEGAL', label: 'Legal contact', contacts: [] },
|
||||
{ value: 'MARKETING', label: 'Marketing contact', contacts: [] },
|
||||
{ value: 'TECH', label: 'Technical contact', contacts: [] },
|
||||
{ value: 'WHOIS', label: 'WHOIS-Inquiry contact', contacts: [] },
|
||||
];
|
||||
|
||||
@Component({
|
||||
selector: 'app-contact-details-dialog',
|
||||
templateUrl: 'contactDetails.component.html',
|
||||
styleUrls: ['./contact.component.scss'],
|
||||
})
|
||||
export class ContactDetailsDialogComponent implements DialogBottomSheetContent {
|
||||
contact?: Contact;
|
||||
contactTypes = contactTypes;
|
||||
contactIndex?: number;
|
||||
|
||||
params?: ContactDetailsParams;
|
||||
|
||||
constructor(
|
||||
public contactService: ContactService,
|
||||
private _snackBar: MatSnackBar
|
||||
) {}
|
||||
|
||||
init(params: ContactDetailsParams) {
|
||||
this.params = params;
|
||||
this.contactIndex = this.contactService
|
||||
.contacts()
|
||||
.findIndex((c) => c === params.data.contact);
|
||||
this.contact = JSON.parse(JSON.stringify(params.data.contact));
|
||||
}
|
||||
|
||||
close() {
|
||||
this.params?.close();
|
||||
}
|
||||
|
||||
saveAndClose(e: SubmitEvent) {
|
||||
e.preventDefault();
|
||||
if (!this.contact || this.contactIndex === undefined) return;
|
||||
if (!(e.target as HTMLFormElement).checkValidity()) {
|
||||
return;
|
||||
}
|
||||
const operation = this.params?.data.operation;
|
||||
let operationObservable;
|
||||
if (operation === Operations.ADD) {
|
||||
operationObservable = this.contactService.addContact(this.contact);
|
||||
} else if (operation === Operations.UPDATE) {
|
||||
operationObservable = this.contactService.updateContact(
|
||||
this.contactIndex,
|
||||
this.contact
|
||||
);
|
||||
} else {
|
||||
throw 'Unknown operation type';
|
||||
}
|
||||
|
||||
operationObservable.subscribe({
|
||||
complete: () => this.close(),
|
||||
error: (err: HttpErrorResponse) => {
|
||||
this._snackBar.open(err.error);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
Contact,
|
||||
ContactService,
|
||||
ViewReadyContact,
|
||||
contactTypeToViewReadyContact,
|
||||
} from './contact.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-contact',
|
||||
@@ -115,61 +30,67 @@ export class ContactDetailsDialogComponent implements DialogBottomSheetContent {
|
||||
})
|
||||
export default class ContactComponent {
|
||||
public static PATH = 'contact';
|
||||
public groupedContacts = computed(() => {
|
||||
return this.contactService.contacts().reduce((acc, contact) => {
|
||||
contact.types.forEach((contactType) => {
|
||||
acc
|
||||
.find((group: GroupedContacts) => group.value === contactType)
|
||||
?.contacts.push(contact);
|
||||
});
|
||||
return acc;
|
||||
}, JSON.parse(JSON.stringify(contactTypes)));
|
||||
});
|
||||
|
||||
@ViewChild('contactDetailsWrapper')
|
||||
detailsComponentWrapper!: DialogBottomSheetWrapper;
|
||||
dataSource: MatTableDataSource<ViewReadyContact> =
|
||||
new MatTableDataSource<ViewReadyContact>([]);
|
||||
columns = [
|
||||
{
|
||||
columnDef: 'name',
|
||||
header: 'Name',
|
||||
cell: (contact: ViewReadyContact) => `
|
||||
<div class="contact__name-column">
|
||||
<div class="contact__name-column-title">${contact.name}</div>
|
||||
<div class="contact__name-column-roles">${contact.userFriendlyTypes.join(
|
||||
' • '
|
||||
)}</div>
|
||||
</div>
|
||||
`,
|
||||
},
|
||||
{
|
||||
columnDef: 'emailAddress',
|
||||
header: 'Email',
|
||||
cell: (contact: ViewReadyContact) => `${contact.emailAddress || ''}`,
|
||||
},
|
||||
{
|
||||
columnDef: 'phoneNumber',
|
||||
header: 'Phone',
|
||||
cell: (contact: ViewReadyContact) => `${contact.phoneNumber || ''}`,
|
||||
},
|
||||
{
|
||||
columnDef: 'faxNumber',
|
||||
header: 'Fax',
|
||||
cell: (contact: ViewReadyContact) => `${contact.faxNumber || ''}`,
|
||||
},
|
||||
];
|
||||
displayedColumns = this.columns.map((c) => c.columnDef);
|
||||
|
||||
loading: boolean = false;
|
||||
constructor(
|
||||
public contactService: ContactService,
|
||||
private _snackBar: MatSnackBar
|
||||
private registrarService: RegistrarService
|
||||
) {
|
||||
// TODO: Refactor to registrarId service
|
||||
this.loading = true;
|
||||
this.contactService.fetchContacts().subscribe(() => {
|
||||
this.loading = false;
|
||||
effect(() => {
|
||||
if (this.registrarService.registrarId()) {
|
||||
this.contactService.isContactDetailsView = false;
|
||||
this.contactService.isContactNewView = false;
|
||||
this.contactService
|
||||
.fetchContacts()
|
||||
.pipe(take(1))
|
||||
.subscribe((contacts) => {
|
||||
this.dataSource = new MatTableDataSource<ViewReadyContact>(
|
||||
contacts.map(contactTypeToViewReadyContact)
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
deleteContact(contact: Contact) {
|
||||
if (confirm(`Please confirm contact ${contact.name} delete`)) {
|
||||
this.contactService.deleteContact(contact).subscribe({
|
||||
error: (err: HttpErrorResponse) => {
|
||||
this._snackBar.open(err.error);
|
||||
},
|
||||
});
|
||||
}
|
||||
openDetails(contact: Contact) {
|
||||
this.contactService.setEditableContact(contact);
|
||||
this.contactService.isContactDetailsView = true;
|
||||
}
|
||||
|
||||
openCreateNew(e: MouseEvent) {
|
||||
const newContact: Contact = {
|
||||
name: '',
|
||||
phoneNumber: '',
|
||||
emailAddress: '',
|
||||
types: [contactTypes[0].value],
|
||||
};
|
||||
this.openDetails(e, newContact, Operations.ADD);
|
||||
}
|
||||
|
||||
openDetails(
|
||||
e: MouseEvent,
|
||||
contact: Contact,
|
||||
operation: Operations = Operations.UPDATE
|
||||
) {
|
||||
e.preventDefault();
|
||||
this.detailsComponentWrapper.open<ContactDetailsDialogComponent>(
|
||||
ContactDetailsDialogComponent,
|
||||
{ contact, operation }
|
||||
);
|
||||
openNewContact() {
|
||||
this.contactService.setEditableContact();
|
||||
this.contactService.isContactNewView = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
||||
// 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.
|
||||
@@ -13,65 +13,112 @@
|
||||
// limitations under the License.
|
||||
|
||||
import { Injectable, signal } from '@angular/core';
|
||||
import { Observable, tap } from 'rxjs';
|
||||
import { Observable, switchMap, tap } from 'rxjs';
|
||||
import { RegistrarService } from 'src/app/registrar/registrar.service';
|
||||
import { BackendService } from 'src/app/shared/services/backend.service';
|
||||
|
||||
export type contactType =
|
||||
| 'ADMIN'
|
||||
| 'ABUSE'
|
||||
| 'BILLING'
|
||||
| 'LEGAL'
|
||||
| 'MARKETING'
|
||||
| 'TECH'
|
||||
| 'WHOIS';
|
||||
|
||||
type contactTypesToUserFriendlyTypes = { [type in contactType]: string };
|
||||
|
||||
export const contactTypeToTextMap: contactTypesToUserFriendlyTypes = {
|
||||
ADMIN: 'Primary contact',
|
||||
ABUSE: 'Abuse contact',
|
||||
BILLING: 'Billing contact',
|
||||
LEGAL: 'Legal contact',
|
||||
MARKETING: 'Marketing contact',
|
||||
TECH: 'Technical contact',
|
||||
WHOIS: 'WHOIS-Inquiry contact',
|
||||
};
|
||||
|
||||
type UserFriendlyType = (typeof contactTypeToTextMap)[contactType];
|
||||
|
||||
export interface Contact {
|
||||
name: string;
|
||||
phoneNumber: string;
|
||||
phoneNumber?: string;
|
||||
emailAddress: string;
|
||||
registrarId?: string;
|
||||
faxNumber?: string;
|
||||
types: Array<string>;
|
||||
types: Array<contactType>;
|
||||
visibleInWhoisAsAdmin?: boolean;
|
||||
visibleInWhoisAsTech?: boolean;
|
||||
visibleInDomainWhoisAsAbuse?: boolean;
|
||||
}
|
||||
|
||||
export interface ViewReadyContact extends Contact {
|
||||
userFriendlyTypes: Array<UserFriendlyType>;
|
||||
}
|
||||
|
||||
export function contactTypeToViewReadyContact(c: Contact): ViewReadyContact {
|
||||
return {
|
||||
...c,
|
||||
userFriendlyTypes: c.types?.map((cType) => contactTypeToTextMap[cType]),
|
||||
};
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ContactService {
|
||||
contacts = signal<Contact[]>([]);
|
||||
contacts = signal<ViewReadyContact[]>([]);
|
||||
contactInEdit!: ViewReadyContact;
|
||||
isContactDetailsView: boolean = false;
|
||||
isContactNewView: boolean = false;
|
||||
|
||||
constructor(
|
||||
private backend: BackendService,
|
||||
private registrarService: RegistrarService
|
||||
) {}
|
||||
|
||||
// TODO: Come up with a better handling for registrarId
|
||||
setEditableContact(contact?: Contact) {
|
||||
this.contactInEdit = contactTypeToViewReadyContact(
|
||||
contact
|
||||
? contact
|
||||
: {
|
||||
emailAddress: '',
|
||||
name: '',
|
||||
types: ['ADMIN'],
|
||||
faxNumber: '',
|
||||
phoneNumber: '',
|
||||
registrarId: '',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
fetchContacts(): Observable<Contact[]> {
|
||||
return this.backend.getContacts(this.registrarService.registrarId()).pipe(
|
||||
tap((contacts = []) => {
|
||||
this.contacts.set(contacts);
|
||||
tap((contacts) => {
|
||||
this.contacts.set(contacts.map(contactTypeToViewReadyContact));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
saveContacts(contacts: Contact[]): Observable<Contact[]> {
|
||||
saveContacts(contacts: ViewReadyContact[]): Observable<Contact[]> {
|
||||
return this.backend
|
||||
.postContacts(this.registrarService.registrarId(), contacts)
|
||||
.pipe(
|
||||
tap((_) => {
|
||||
this.contacts.set(contacts);
|
||||
})
|
||||
);
|
||||
.pipe(switchMap((_) => this.fetchContacts()));
|
||||
}
|
||||
|
||||
updateContact(index: number, contact: Contact) {
|
||||
updateContact(index: number, contact: ViewReadyContact) {
|
||||
const newContacts = this.contacts().map((c, i) =>
|
||||
i === index ? contact : c
|
||||
);
|
||||
return this.saveContacts(newContacts);
|
||||
}
|
||||
|
||||
addContact(contact: Contact) {
|
||||
addContact(contact: ViewReadyContact) {
|
||||
const newContacts = this.contacts().concat([contact]);
|
||||
return this.saveContacts(newContacts);
|
||||
}
|
||||
|
||||
deleteContact(contact: Contact) {
|
||||
deleteContact(contact: ViewReadyContact) {
|
||||
const newContacts = this.contacts().filter((c) => c !== contact);
|
||||
return this.saveContacts(newContacts);
|
||||
}
|
||||
|
||||
@@ -1,104 +1,201 @@
|
||||
<h3 mat-dialog-title>Contact details</h3>
|
||||
<div mat-dialog-content *ngIf="contact">
|
||||
<form (ngSubmit)="saveAndClose($event)">
|
||||
<p>
|
||||
<mat-form-field class="contact-details__input">
|
||||
<div class="console-app__contact" *ngIf="contactService.contactInEdit">
|
||||
<div class="console-app__contact-controls">
|
||||
<button
|
||||
mat-icon-button
|
||||
aria-label="Back to contacts list"
|
||||
(click)="goBack()"
|
||||
>
|
||||
<mat-icon>arrow_back</mat-icon>
|
||||
</button>
|
||||
<div class="spacer"></div>
|
||||
@if(!isEditing && !contactService.isContactNewView) {
|
||||
<button
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
aria-label="Edit Contact"
|
||||
(click)="isEditing = true"
|
||||
>
|
||||
<mat-icon>edit</mat-icon>
|
||||
Edit
|
||||
</button>
|
||||
<button mat-icon-button aria-label="Delete Contact">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if(isEditing || contactService.isContactNewView) {
|
||||
<h1>Contact Details</h1>
|
||||
<form (ngSubmit)="save($event)">
|
||||
<section>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Name: </mat-label>
|
||||
<input
|
||||
matInput
|
||||
[required]="true"
|
||||
[(ngModel)]="contact.name"
|
||||
[(ngModel)]="contactService.contactInEdit.name"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<mat-form-field class="contact-details__input">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Primary account email: </mat-label>
|
||||
<input
|
||||
type="email"
|
||||
matInput
|
||||
[email]="true"
|
||||
[required]="true"
|
||||
[(ngModel)]="contact.emailAddress"
|
||||
[(ngModel)]="contactService.contactInEdit.emailAddress"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<mat-form-field class="contact-details__input">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Phone: </mat-label>
|
||||
<input
|
||||
matInput
|
||||
[(ngModel)]="contact.phoneNumber"
|
||||
[(ngModel)]="contactService.contactInEdit.phoneNumber"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
placeholder="+0.0000000000"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<mat-form-field class="contact-details__input">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Fax: </mat-label>
|
||||
<input
|
||||
matInput
|
||||
[(ngModel)]="contact.faxNumber"
|
||||
[(ngModel)]="contactService.contactInEdit.faxNumber"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<div class="contact-details__group">
|
||||
<label>Contact type:</label>
|
||||
<div class="contact-details__group-content">
|
||||
<section>
|
||||
<h1>Contact Type</h1>
|
||||
<p class="console-app__contact-required">
|
||||
<mat-icon color="accent">error</mat-icon>Required to select at least one
|
||||
</p>
|
||||
<div class="">
|
||||
<mat-checkbox
|
||||
*ngFor="let contactType of contactTypes"
|
||||
[checked]="contact.types.includes(contactType.value)"
|
||||
(change)="
|
||||
$event.checked
|
||||
? contact.types.push(contactType.value)
|
||||
: contact.types.splice(
|
||||
contact.types.indexOf(contactType.value),
|
||||
1
|
||||
)
|
||||
"
|
||||
[disabled]="
|
||||
contact.types.length === 1 && contact.types[0] === contactType.value
|
||||
"
|
||||
*ngFor="let contactType of contactTypeToTextMap | keyvalue"
|
||||
[checked]="checkboxIsChecked(contactType.key)"
|
||||
(change)="checkboxOnChange($event, contactType.key)"
|
||||
[disabled]="checkboxIsDisabled(contactType.key)"
|
||||
>
|
||||
{{ contactType.label }}
|
||||
{{ contactType.value }}
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section>
|
||||
<mat-checkbox
|
||||
[(ngModel)]="contact.visibleInWhoisAsAdmin"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
>Show in Registrar WHOIS record as admin contact</mat-checkbox
|
||||
>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<mat-checkbox
|
||||
[(ngModel)]="contact.visibleInWhoisAsTech"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
>Show in Registrar WHOIS record as technical contact</mat-checkbox
|
||||
>
|
||||
</section>
|
||||
<h1>WHOIS Preferences</h1>
|
||||
<div>
|
||||
<mat-checkbox
|
||||
[(ngModel)]="contactService.contactInEdit.visibleInWhoisAsAdmin"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
>Show in Registrar WHOIS record as admin contact</mat-checkbox
|
||||
>
|
||||
</div>
|
||||
|
||||
<section>
|
||||
<mat-checkbox
|
||||
[(ngModel)]="contact.visibleInDomainWhoisAsAbuse"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
>Show Phone and Email in Domain WHOIS Record as registrar abuse contact
|
||||
(per CL&D requirements)</mat-checkbox
|
||||
>
|
||||
<div>
|
||||
<mat-checkbox
|
||||
[(ngModel)]="contactService.contactInEdit.visibleInWhoisAsTech"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
>Show in Registrar WHOIS record as technical contact</mat-checkbox
|
||||
>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<mat-checkbox
|
||||
[(ngModel)]="contactService.contactInEdit.visibleInDomainWhoisAsAbuse"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
>Show Phone and Email in Domain WHOIS Record as registrar abuse
|
||||
contact (per CL&D requirements)</mat-checkbox
|
||||
>
|
||||
</div>
|
||||
</section>
|
||||
<mat-dialog-actions>
|
||||
<button mat-button (click)="close()">Cancel</button>
|
||||
<button type="submit" mat-button>Save</button>
|
||||
</mat-dialog-actions>
|
||||
<button
|
||||
class="console-app__contact-submit"
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
type="submit"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</form>
|
||||
} @else {
|
||||
<h1>{{ contactService.contactInEdit.name }}</h1>
|
||||
<mat-card appearance="outlined">
|
||||
<mat-card-content>
|
||||
<mat-list role="list">
|
||||
<mat-list-item role="listitem">
|
||||
<h2>Contact details</h2>
|
||||
</mat-list-item>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-list-item role="listitem">
|
||||
<span class="console-app__list-key">Email</span>
|
||||
<span class="console-app__list-value">{{
|
||||
contactService.contactInEdit.emailAddress
|
||||
}}</span>
|
||||
</mat-list-item>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-list-item role="listitem">
|
||||
<span class="console-app__list-key">Phone</span>
|
||||
<span class="console-app__list-value">{{
|
||||
contactService.contactInEdit.phoneNumber
|
||||
}}</span>
|
||||
</mat-list-item>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-list-item role="listitem">
|
||||
<span class="console-app__list-key">Fax</span>
|
||||
<span class="console-app__list-value">{{
|
||||
contactService.contactInEdit.faxNumber
|
||||
}}</span>
|
||||
</mat-list-item>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-list-item role="listitem">
|
||||
<span class="console-app__list-key">Type:</span>
|
||||
<span class="console-app__list-value">{{
|
||||
contactService.contactInEdit.userFriendlyTypes.join(", ")
|
||||
}}</span>
|
||||
</mat-list-item>
|
||||
</mat-list>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<mat-card appearance="outlined">
|
||||
<mat-card-content>
|
||||
<mat-list role="list">
|
||||
<mat-list-item role="listitem">
|
||||
<h2>WHOIS Preferences</h2>
|
||||
</mat-list-item>
|
||||
@if(contactService.contactInEdit.visibleInWhoisAsAdmin) {
|
||||
<mat-divider></mat-divider>
|
||||
<mat-list-item role="listitem">
|
||||
<span class="console-app__list-value"
|
||||
>Show in Registrar WHOIS record as admin contact</span
|
||||
>
|
||||
</mat-list-item>
|
||||
} @if(contactService.contactInEdit.visibleInWhoisAsTech) {
|
||||
<mat-divider></mat-divider>
|
||||
<mat-list-item
|
||||
role="listitem"
|
||||
*ngIf="contactService.contactInEdit.visibleInWhoisAsTech"
|
||||
>
|
||||
<span class="console-app__list-value"
|
||||
>Show in Registrar WHOIS record as technical contact</span
|
||||
>
|
||||
</mat-list-item>
|
||||
} @if(contactService.contactInEdit.visibleInDomainWhoisAsAbuse) {
|
||||
<mat-divider></mat-divider>
|
||||
<mat-list-item role="listitem">
|
||||
<span class="console-app__list-value"
|
||||
>Show Phone and Email in Domain WHOIS Record as registrar abuse
|
||||
contact (per CL&D requirements)</span
|
||||
>
|
||||
</mat-list-item>
|
||||
}
|
||||
</mat-list>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
.console-app__contact {
|
||||
max-width: 616px;
|
||||
section {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
&-required {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
&-submit {
|
||||
margin: 30px 0;
|
||||
}
|
||||
|
||||
&-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
mat-card {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
mat-form-field {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
// 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 } from '@angular/core';
|
||||
import { MatCheckboxChange } from '@angular/material/checkbox';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { first } from 'rxjs';
|
||||
import {
|
||||
ContactService,
|
||||
contactType,
|
||||
contactTypeToTextMap,
|
||||
} from './contact.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-contact-details',
|
||||
templateUrl: './contactDetails.component.html',
|
||||
styleUrls: ['./contactDetails.component.scss'],
|
||||
})
|
||||
export class ContactDetailsComponent {
|
||||
protected contactTypeToTextMap = contactTypeToTextMap;
|
||||
isEditing: boolean = false;
|
||||
|
||||
constructor(
|
||||
protected contactService: ContactService,
|
||||
private _snackBar: MatSnackBar
|
||||
) {}
|
||||
|
||||
deleteContact() {
|
||||
if (
|
||||
confirm(
|
||||
`Please confirm deletion of contact ${this.contactService.contactInEdit.name}`
|
||||
)
|
||||
) {
|
||||
this.contactService
|
||||
.deleteContact(this.contactService.contactInEdit)
|
||||
.pipe(first())
|
||||
.subscribe({
|
||||
error: (err: HttpErrorResponse) => {
|
||||
this._snackBar.open(err.error);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
goBack() {
|
||||
if (this.isEditing) {
|
||||
this.isEditing = false;
|
||||
} else {
|
||||
this.contactService.isContactNewView = false;
|
||||
this.contactService.isContactDetailsView = false;
|
||||
}
|
||||
}
|
||||
|
||||
save(e: SubmitEvent) {
|
||||
e.preventDefault();
|
||||
this.contactService
|
||||
.saveContacts([this.contactService.contactInEdit])
|
||||
.subscribe({
|
||||
complete: () => {
|
||||
this.goBack();
|
||||
},
|
||||
error: (err: HttpErrorResponse) => {
|
||||
this._snackBar.open(err.error);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
checkboxIsChecked(type: string) {
|
||||
return this.contactService.contactInEdit.types.includes(
|
||||
type as contactType
|
||||
);
|
||||
}
|
||||
|
||||
checkboxIsDisabled(type: string) {
|
||||
return (
|
||||
this.contactService.contactInEdit.types.length === 1 &&
|
||||
this.contactService.contactInEdit.types[0] === (type as contactType)
|
||||
);
|
||||
}
|
||||
|
||||
checkboxOnChange(event: MatCheckboxChange, type: string) {
|
||||
if (event.checked) {
|
||||
this.contactService.contactInEdit.types.push(type as contactType);
|
||||
} else {
|
||||
this.contactService.contactInEdit.types =
|
||||
this.contactService.contactInEdit.types.filter(
|
||||
(t) => t != (type as contactType)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,98 +1,82 @@
|
||||
<div *ngIf="loading" class="settings-security__loading">
|
||||
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
|
||||
</div>
|
||||
<div class="settings-security" *ngIf="!loading">
|
||||
<div class="settings-security__section">
|
||||
<div class="settings-security__section-description">
|
||||
<h2>IP Allowlist</h2>
|
||||
<p>
|
||||
Restrict access to EPP production servers to the following IP/IPv6
|
||||
addresses, or ranges like 1.1.1.0/24
|
||||
</p>
|
||||
</div>
|
||||
<div class="settings-security__section-form">
|
||||
<div
|
||||
*ngIf="
|
||||
dataSource.ipAddressAllowList &&
|
||||
dataSource.ipAddressAllowList.length > 0
|
||||
"
|
||||
>
|
||||
<div
|
||||
*ngFor="let item of dataSource.ipAddressAllowList; index as index"
|
||||
class="settings-security__ipRecord"
|
||||
>
|
||||
<mat-form-field>
|
||||
<input
|
||||
matInput
|
||||
type="text"
|
||||
class="settings-security__ip-allowlist"
|
||||
[(ngModel)]="item.value"
|
||||
[disabled]="!inEdit"
|
||||
/>
|
||||
@if(securityService.isEditingSecurity) {
|
||||
<app-security-edit></app-security-edit>
|
||||
} @else {
|
||||
<div class="settings-security">
|
||||
<mat-card appearance="outlined">
|
||||
<mat-card-content>
|
||||
<mat-list role="list">
|
||||
<!-- IP Allowlist Start -->
|
||||
<mat-list-item role="listitem">
|
||||
<div class="settings-security__section-header">
|
||||
<h2>IP Allowlist</h2>
|
||||
<button
|
||||
*ngIf="inEdit"
|
||||
matSuffix
|
||||
mat-icon-button
|
||||
aria-label="Remove"
|
||||
(click)="removeIpEntry(index)"
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
aria-label="Edit Contact"
|
||||
(click)="editSecurity()"
|
||||
>
|
||||
<mat-icon>close</mat-icon>
|
||||
<mat-icon>edit</mat-icon>
|
||||
Edit
|
||||
</button>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
<button mat-stroked-button (click)="enableEdit(); createIpEntry()">
|
||||
Add IP
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-security__section">
|
||||
<div class="settings-security__section-description">
|
||||
<h2>SSL Certificate</h2>
|
||||
<p>X.509 PEM certificate for EPP production access.</p>
|
||||
</div>
|
||||
<div class="settings-security__section-form">
|
||||
<textarea
|
||||
matInput
|
||||
class="settings-security__clientCertificate"
|
||||
[(ngModel)]="dataSource.clientCertificate"
|
||||
[disabled]="!inEdit"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-security__section">
|
||||
<div class="settings-security__section-description">
|
||||
<h2>Failover SSL Certificate</h2>
|
||||
<p>X.509 PEM backup certificate for EPP Production Access.</p>
|
||||
</div>
|
||||
<div class="settings-security__section-form">
|
||||
<textarea
|
||||
matInput
|
||||
[(ngModel)]="dataSource.failoverClientCertificate"
|
||||
[disabled]="!inEdit"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-security__actions">
|
||||
<ng-template [ngIf]="inEdit" [ngIfElse]="inView">
|
||||
<button
|
||||
class="settings-security__actions-save"
|
||||
mat-raised-button
|
||||
color="primary"
|
||||
(click)="save()"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
class="settings-security__actions-cancel"
|
||||
mat-stroked-button
|
||||
(click)="cancel()"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</ng-template>
|
||||
<ng-template #inView>
|
||||
<button #elseBlock mat-raised-button (click)="enableEdit()">Edit</button>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
</mat-list-item>
|
||||
<mat-list-item role="listitem" lines="3">
|
||||
<span class="console-app__list-value"
|
||||
>Restrict access to EPP production servers to the following IP/IPv6
|
||||
addresses, or ranges like 1.1.1.0/24</span
|
||||
>
|
||||
</mat-list-item>
|
||||
<mat-divider></mat-divider>
|
||||
@for (item of dataSource.ipAddressAllowList; track item.value) {
|
||||
<mat-list-item role="listitem">
|
||||
<span class="console-app__list-value">{{ item.value }}</span>
|
||||
</mat-list-item>
|
||||
<mat-divider></mat-divider>
|
||||
} @empty {
|
||||
<mat-list-item role="listitem">
|
||||
<span class="console-app__list-value">No IP addresses on file.</span>
|
||||
</mat-list-item>
|
||||
}
|
||||
<mat-list-item role="listitem"></mat-list-item>
|
||||
<!-- IP Allowlist End -->
|
||||
|
||||
<!-- SSL Certificate Start -->
|
||||
<mat-list-item role="listitem">
|
||||
<h2>SSL Certificate</h2>
|
||||
</mat-list-item>
|
||||
<mat-list-item role="listitem">
|
||||
<span class="console-app__list-value"
|
||||
>X.509 PEM certificate for EPP production access</span
|
||||
>
|
||||
</mat-list-item>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-list-item role="listitem" lines="10">
|
||||
<span class="console-app__list-value">{{
|
||||
dataSource.clientCertificate || "No client certificate on file."
|
||||
}}</span>
|
||||
</mat-list-item>
|
||||
<mat-list-item role="listitem"> </mat-list-item>
|
||||
<!-- SSL Certificate End -->
|
||||
|
||||
<!-- Failover SSL Certificate Start -->
|
||||
<mat-list-item role="listitem">
|
||||
<h2>Failover SSL Certificate</h2>
|
||||
</mat-list-item>
|
||||
<mat-list-item role="listitem">
|
||||
<span class="console-app__list-value"
|
||||
>X.509 PEM backup certificate for EPP production access</span
|
||||
>
|
||||
</mat-list-item>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-list-item role="listitem" lines="10">
|
||||
<span class="console-app__list-value">{{
|
||||
dataSource.failoverClientCertificate ||
|
||||
"No failover certificate on file."
|
||||
}}</span>
|
||||
</mat-list-item>
|
||||
<!-- Failover SSL Certificate End -->
|
||||
</mat-list>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
||||
// 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.
|
||||
@@ -13,41 +13,14 @@
|
||||
// limitations under the License.
|
||||
|
||||
.settings-security {
|
||||
margin-top: 1.5rem;
|
||||
h1 {
|
||||
margin: 0;
|
||||
max-width: 616px;
|
||||
mat-card {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
&__ipRecord {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
&__section {
|
||||
&__section-header {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
margin-bottom: 3rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
&__section-description {
|
||||
flex: 1;
|
||||
min-width: 300px;
|
||||
}
|
||||
&__section-form {
|
||||
flex: 1;
|
||||
min-width: 300px;
|
||||
textarea {
|
||||
min-height: 100%;
|
||||
min-width: 100%;
|
||||
box-sizing: border-box;
|
||||
min-height: 100px;
|
||||
}
|
||||
}
|
||||
&__actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
button {
|
||||
margin-left: 20px;
|
||||
}
|
||||
}
|
||||
&__loading {
|
||||
margin: 2rem 0;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
||||
// 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.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
||||
// 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.
|
||||
@@ -12,70 +12,35 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component } from '@angular/core';
|
||||
import { Component, effect } from '@angular/core';
|
||||
import { RegistrarService } from 'src/app/registrar/registrar.service';
|
||||
import {
|
||||
SecurityService,
|
||||
SecuritySettings,
|
||||
apiToUiConverter,
|
||||
} from './security.service';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { RegistrarService } from 'src/app/registrar/registrar.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-security',
|
||||
templateUrl: './security.component.html',
|
||||
styleUrls: ['./security.component.scss'],
|
||||
providers: [SecurityService],
|
||||
})
|
||||
export default class SecurityComponent {
|
||||
public static PATH = 'security';
|
||||
|
||||
loading: boolean = false;
|
||||
inEdit: boolean = false;
|
||||
dataSource: SecuritySettings = {};
|
||||
|
||||
constructor(
|
||||
public securityService: SecurityService,
|
||||
private _snackBar: MatSnackBar,
|
||||
public registrarService: RegistrarService
|
||||
) {
|
||||
this.dataSource = apiToUiConverter(this.registrarService.registrar());
|
||||
}
|
||||
|
||||
enableEdit() {
|
||||
this.inEdit = true;
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.inEdit = false;
|
||||
this.resetDataSource();
|
||||
}
|
||||
|
||||
createIpEntry() {
|
||||
this.dataSource.ipAddressAllowList?.push({ value: '' });
|
||||
}
|
||||
|
||||
save() {
|
||||
this.loading = true;
|
||||
this.securityService.saveChanges(this.dataSource).subscribe({
|
||||
complete: () => {
|
||||
this.loading = false;
|
||||
this.resetDataSource();
|
||||
},
|
||||
error: (err: HttpErrorResponse) => {
|
||||
this._snackBar.open(err.error);
|
||||
},
|
||||
effect(() => {
|
||||
if (this.registrarService.registrar()) {
|
||||
this.dataSource = apiToUiConverter(this.registrarService.registrar());
|
||||
this.securityService.isEditingSecurity = false;
|
||||
}
|
||||
});
|
||||
this.cancel();
|
||||
}
|
||||
|
||||
removeIpEntry(index: number) {
|
||||
this.dataSource.ipAddressAllowList =
|
||||
this.dataSource.ipAddressAllowList?.filter((_, i) => i != index);
|
||||
}
|
||||
|
||||
resetDataSource() {
|
||||
this.dataSource = apiToUiConverter(this.registrarService.registrar());
|
||||
editSecurity() {
|
||||
this.securityService.isEditingSecurity = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
||||
// 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.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
||||
// 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.
|
||||
@@ -17,7 +17,7 @@ import { switchMap } from 'rxjs';
|
||||
import { RegistrarService } from 'src/app/registrar/registrar.service';
|
||||
import { BackendService } from 'src/app/shared/services/backend.service';
|
||||
|
||||
interface ipAllowListItem {
|
||||
export interface ipAllowListItem {
|
||||
value: string;
|
||||
}
|
||||
export interface SecuritySettings {
|
||||
@@ -52,9 +52,12 @@ export function uiToApiConverter(
|
||||
});
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class SecurityService {
|
||||
securitySettings: SecuritySettings = {};
|
||||
isEditingSecurity: boolean = false;
|
||||
|
||||
constructor(
|
||||
private backend: BackendService,
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
<div class="settings-security__edit">
|
||||
<h1>IP Allowlist</h1>
|
||||
<p>
|
||||
Restrict access to EPP production servers to the following IP/IPv6
|
||||
addresses, or ranges like 1.1.1.0/24
|
||||
</p>
|
||||
<form (ngSubmit)="save()">
|
||||
@for (ip of dataSource.ipAddressAllowList; track ip.value) {
|
||||
<div class="settings-security__edit-ip">
|
||||
<mat-form-field appearance="outline">
|
||||
<input
|
||||
matInput
|
||||
type="text"
|
||||
[(ngModel)]="ip.value"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
/>
|
||||
</mat-form-field>
|
||||
<button
|
||||
matSuffix
|
||||
mat-icon-button
|
||||
aria-label="Remove"
|
||||
(click)="removeIpEntry(ip)"
|
||||
>
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
<button mat-button color="primary" (click)="createIpEntry()" type="button">
|
||||
+ Add IP
|
||||
</button>
|
||||
|
||||
<h1>SSL Certificate</h1>
|
||||
<p>X.509 PEM certificate for EPP production access.</p>
|
||||
<mat-form-field appearance="outline">
|
||||
<textarea
|
||||
matInput
|
||||
[(ngModel)]="dataSource.clientCertificate"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
></textarea>
|
||||
</mat-form-field>
|
||||
<h1>Failover SSL Certificate</h1>
|
||||
<p>X.509 PEM backup certificate for EPP Production Access.</p>
|
||||
<mat-form-field appearance="outline">
|
||||
<textarea
|
||||
matInput
|
||||
[(ngModel)]="dataSource.failoverClientCertificate"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
></textarea>
|
||||
</mat-form-field>
|
||||
<button
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
aria-label="Save security settings"
|
||||
type="submit"
|
||||
class="settings-security__edit-save"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -0,0 +1,33 @@
|
||||
// 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.
|
||||
|
||||
.settings-security__edit {
|
||||
max-width: 616px;
|
||||
h1 {
|
||||
margin-top: 30px;
|
||||
}
|
||||
mat-form-field {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
}
|
||||
&-ip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
&-save {
|
||||
margin-top: 30px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
// 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 } from '@angular/core';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { RegistrarService } from 'src/app/registrar/registrar.service';
|
||||
import {
|
||||
SecurityService,
|
||||
SecuritySettings,
|
||||
apiToUiConverter,
|
||||
ipAllowListItem,
|
||||
} from './security.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-security-edit',
|
||||
templateUrl: './securityEdit.component.html',
|
||||
styleUrls: ['./securityEdit.component.scss'],
|
||||
})
|
||||
export default class SecurityEditComponent {
|
||||
dataSource: SecuritySettings = {};
|
||||
|
||||
constructor(
|
||||
public securityService: SecurityService,
|
||||
private _snackBar: MatSnackBar,
|
||||
public registrarService: RegistrarService
|
||||
) {
|
||||
this.dataSource = apiToUiConverter(registrarService.registrar());
|
||||
}
|
||||
|
||||
createIpEntry() {
|
||||
this.dataSource.ipAddressAllowList?.push({ value: '' });
|
||||
}
|
||||
|
||||
save() {
|
||||
this.securityService.saveChanges(this.dataSource).subscribe({
|
||||
complete: () => {
|
||||
this.goBack();
|
||||
},
|
||||
error: (err: HttpErrorResponse) => {
|
||||
this._snackBar.open(err.error);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
goBack() {
|
||||
this.securityService.isEditingSecurity = false;
|
||||
}
|
||||
|
||||
removeIpEntry(ip: ipAllowListItem) {
|
||||
this.dataSource.ipAddressAllowList =
|
||||
this.dataSource.ipAddressAllowList?.filter((item) => item !== ip);
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,37 @@
|
||||
<div class="console-settings">
|
||||
<h1>Settings</h1>
|
||||
<nav mat-tab-nav-bar [tabPanel]="tabPanel">
|
||||
<a mat-tab-link routerLink="contact" routerLinkActive="active-link"
|
||||
>Contact Info</a
|
||||
<app-selected-registrar-wrapper>
|
||||
<div class="console-settings">
|
||||
<h1 class="mat-headline-4">Settings</h1>
|
||||
<nav
|
||||
mat-tab-nav-bar
|
||||
mat-stretch-tabs="false"
|
||||
mat-align-tabs="start"
|
||||
[tabPanel]="tabPanel"
|
||||
>
|
||||
<a mat-tab-link routerLink="whois" routerLinkActive="active-link"
|
||||
>WHOIS Info</a
|
||||
>
|
||||
<a mat-tab-link routerLink="security" routerLinkActive="active-link"
|
||||
>Security</a
|
||||
>
|
||||
<a mat-tab-link routerLink="epp-password" routerLinkActive="active-link"
|
||||
>EPP Password</a
|
||||
>
|
||||
<a mat-tab-link routerLink="users" routerLinkActive="active-link">Users</a>
|
||||
<a mat-tab-link routerLink="registrars" routerLinkActive="active-link"
|
||||
>Registrars</a
|
||||
>
|
||||
</nav>
|
||||
<mat-tab-nav-panel #tabPanel>
|
||||
<router-outlet></router-outlet>
|
||||
</mat-tab-nav-panel>
|
||||
</div>
|
||||
<a
|
||||
mat-tab-link
|
||||
routerLink="contact"
|
||||
routerLinkActive="active-link"
|
||||
queryParamsHandling="merge"
|
||||
>Contacts</a
|
||||
>
|
||||
<a
|
||||
mat-tab-link
|
||||
routerLink="whois"
|
||||
routerLinkActive="active-link"
|
||||
queryParamsHandling="merge"
|
||||
>WHOIS Info</a
|
||||
>
|
||||
<a
|
||||
mat-tab-link
|
||||
routerLink="security"
|
||||
routerLinkActive="active-link"
|
||||
queryParamsHandling="merge"
|
||||
>Security</a
|
||||
>
|
||||
</nav>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-tab-nav-panel #tabPanel>
|
||||
<router-outlet></router-outlet>
|
||||
</mat-tab-nav-panel>
|
||||
</div>
|
||||
</app-selected-registrar-wrapper>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
||||
// 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.
|
||||
@@ -13,6 +13,9 @@
|
||||
// limitations under the License.
|
||||
|
||||
.console-settings {
|
||||
> mat-divider {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
.mdc-tab {
|
||||
&.active-link {
|
||||
border-bottom: 2px solid var(--primary);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
||||
// 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.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
||||
// 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.
|
||||
@@ -22,4 +22,10 @@ import { Component, ViewEncapsulation } from '@angular/core';
|
||||
})
|
||||
export class SettingsComponent {
|
||||
public static PATH = 'settings';
|
||||
|
||||
public static matchesUrl(url: string): boolean {
|
||||
return url[0] === '/'
|
||||
? url.startsWith(`/${this.PATH}`)
|
||||
: url.startsWith(this.PATH);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
||||
// 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.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
||||
// 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.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
||||
// 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.
|
||||
|
||||
@@ -1,244 +1,105 @@
|
||||
<div class="settings-whois">
|
||||
<h2>WHOIS settings</h2>
|
||||
<h3>
|
||||
General registrar information for your WHOIS record. This information is
|
||||
always visible in WHOIS.
|
||||
</h3>
|
||||
<div *ngIf="loading" class="settings-whois__loading">
|
||||
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
|
||||
</div>
|
||||
<div class="settings-whois__section">
|
||||
<div class="settings-whois__section-description">
|
||||
<h3>Name:</h3>
|
||||
</div>
|
||||
<div class="settings-whois__section-form">
|
||||
<mat-form-field>
|
||||
<input
|
||||
matInput
|
||||
type="text"
|
||||
[(ngModel)]="registrar.registrarName"
|
||||
disabled
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-whois__section">
|
||||
<div class="settings-whois__section-description">
|
||||
<h3>IANA Identifier:</h3>
|
||||
</div>
|
||||
<div class="settings-whois__section-form">
|
||||
<mat-form-field>
|
||||
<input
|
||||
matInput
|
||||
type="text"
|
||||
[(ngModel)]="registrar.ianaIdentifier"
|
||||
disabled
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-whois__section">
|
||||
<div class="settings-whois__section-description">
|
||||
<h3>ICANN Referral Email:</h3>
|
||||
</div>
|
||||
<div class="settings-whois__section-form">
|
||||
<mat-form-field>
|
||||
<input
|
||||
matInput
|
||||
type="email"
|
||||
[(ngModel)]="registrar.icannReferralEmail"
|
||||
disabled
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-whois__section">
|
||||
<div class="settings-whois__section-description">
|
||||
<h3>WHOIS server:</h3>
|
||||
</div>
|
||||
<div class="settings-whois__section-form">
|
||||
<mat-form-field>
|
||||
<input
|
||||
matInput
|
||||
type="text"
|
||||
[(ngModel)]="registrar.whoisServer"
|
||||
[disabled]="!inEdit"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-whois__section">
|
||||
<div class="settings-whois__section-description">
|
||||
<h3>Referral URL:</h3>
|
||||
</div>
|
||||
<div class="settings-whois__section-form">
|
||||
<mat-form-field>
|
||||
<input
|
||||
matInput
|
||||
type="text"
|
||||
[(ngModel)]="registrar.url"
|
||||
[disabled]="!inEdit"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-whois__section">
|
||||
<div class="settings-whois__section-description">
|
||||
<h3>Email:</h3>
|
||||
</div>
|
||||
<div class="settings-whois__section-form">
|
||||
<mat-form-field>
|
||||
<input
|
||||
matInput
|
||||
type="email"
|
||||
[(ngModel)]="registrar.emailAddress"
|
||||
[disabled]="!inEdit"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-whois__section">
|
||||
<div class="settings-whois__section-description">
|
||||
<h3>Phone::</h3>
|
||||
</div>
|
||||
<div class="settings-whois__section-form">
|
||||
<mat-form-field>
|
||||
<input
|
||||
matInput
|
||||
type="text"
|
||||
[(ngModel)]="registrar.phoneNumber"
|
||||
[disabled]="!inEdit"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-whois__section">
|
||||
<div class="settings-whois__section-description">
|
||||
<h3>Fax:</h3>
|
||||
</div>
|
||||
<div class="settings-whois__section-form">
|
||||
<mat-form-field>
|
||||
<input
|
||||
matInput
|
||||
type="text"
|
||||
[(ngModel)]="registrar.faxNumber"
|
||||
[disabled]="!inEdit"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-whois__section">
|
||||
<div class="settings-whois__section-address">
|
||||
<div class="settings-whois__section-description">
|
||||
<h3>Address Line 1:</h3>
|
||||
</div>
|
||||
<div class="settings-whois__section-form">
|
||||
<mat-form-field *ngIf="registrar.localizedAddress?.street">
|
||||
<input
|
||||
matInput
|
||||
type="text"
|
||||
[(ngModel)]="(registrar.localizedAddress?.street)![0]"
|
||||
[disabled]="!inEdit"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-whois__section-address">
|
||||
<div class="settings-whois__section-description">
|
||||
<h3>City:</h3>
|
||||
</div>
|
||||
<div class="settings-whois__section-form">
|
||||
<mat-form-field *ngIf="registrar.localizedAddress">
|
||||
<input
|
||||
matInput
|
||||
type="text"
|
||||
[(ngModel)]="registrar.localizedAddress.city"
|
||||
[disabled]="!inEdit"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-whois__section">
|
||||
<div class="settings-whois__section-address">
|
||||
<div class="settings-whois__section-description">
|
||||
<h3>Address Line 2:</h3>
|
||||
</div>
|
||||
<div class="settings-whois__section-form">
|
||||
<mat-form-field *ngIf="registrar.localizedAddress?.street">
|
||||
<input
|
||||
matInput
|
||||
type="text"
|
||||
[(ngModel)]="(registrar.localizedAddress?.street)![1]"
|
||||
[disabled]="!inEdit"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-whois__section-address">
|
||||
<div class="settings-whois__section-description">
|
||||
<h3>State/Region:</h3>
|
||||
</div>
|
||||
<div class="settings-whois__section-form">
|
||||
<mat-form-field *ngIf="registrar.localizedAddress">
|
||||
<input
|
||||
matInput
|
||||
type="text"
|
||||
[(ngModel)]="registrar.localizedAddress.state"
|
||||
[disabled]="!inEdit"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-whois__section">
|
||||
<div class="settings-whois__section-address">
|
||||
<div class="settings-whois__section-description">
|
||||
<h3>Address Line 3:</h3>
|
||||
</div>
|
||||
<div class="settings-whois__section-form">
|
||||
<mat-form-field *ngIf="registrar.localizedAddress?.street">
|
||||
<input
|
||||
matInput
|
||||
type="text"
|
||||
[(ngModel)]="(registrar.localizedAddress?.street)![2]"
|
||||
[disabled]="!inEdit"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-whois__section-address">
|
||||
<div class="settings-whois__section-description">
|
||||
<h3>Country Code:</h3>
|
||||
</div>
|
||||
<div class="settings-whois__section-form">
|
||||
<mat-form-field *ngIf="registrar.localizedAddress">
|
||||
<input
|
||||
matInput
|
||||
type="text"
|
||||
[(ngModel)]="registrar.localizedAddress.countryCode"
|
||||
[disabled]="!inEdit"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-whois__actions">
|
||||
<ng-template [ngIf]="inEdit" [ngIfElse]="inView">
|
||||
<button
|
||||
class="actions-save"
|
||||
mat-raised-button
|
||||
color="primary"
|
||||
(click)="save()"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button class="actions-cancel" mat-stroked-button (click)="cancel()">
|
||||
Cancel
|
||||
</button>
|
||||
</ng-template>
|
||||
<ng-template #inView>
|
||||
<button #elseBlock mat-raised-button (click)="enableEdit()">Edit</button>
|
||||
</ng-template>
|
||||
@if(whoisService.editing) {
|
||||
<app-whois-edit></app-whois-edit>
|
||||
} @else {
|
||||
<div class="console-app__whois">
|
||||
<div class="console-app__whois-controls">
|
||||
<span>
|
||||
General registrar information for your WHOIS record. This information is
|
||||
always visible in WHOIS.
|
||||
</span>
|
||||
<div class="spacer"></div>
|
||||
<button
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
aria-label="Edit WHOIS record"
|
||||
(click)="whoisService.editing = true"
|
||||
>
|
||||
<mat-icon>edit</mat-icon>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<mat-card appearance="outlined">
|
||||
<mat-card-content>
|
||||
<mat-list role="list">
|
||||
<mat-list-item role="listitem">
|
||||
<h2>Personal Info</h2>
|
||||
</mat-list-item>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-list-item role="listitem">
|
||||
<span class="console-app__list-key">Company name</span>
|
||||
<span class="console-app__list-value">{{
|
||||
registrarService.registrar()?.registrarName
|
||||
}}</span>
|
||||
</mat-list-item>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-list-item role="listitem">
|
||||
<span class="console-app__list-key">Referral email</span>
|
||||
<span class="console-app__list-value">{{
|
||||
registrarService.registrar()?.emailAddress
|
||||
}}</span>
|
||||
</mat-list-item>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-list-item role="listitem">
|
||||
<span class="console-app__list-key">Phone</span>
|
||||
<span class="console-app__list-value">{{
|
||||
registrarService.registrar()?.phoneNumber
|
||||
}}</span>
|
||||
</mat-list-item>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-list-item role="listitem">
|
||||
<span class="console-app__list-key">Fax</span>
|
||||
<span class="console-app__list-value">{{
|
||||
registrarService.registrar()?.faxNumber
|
||||
}}</span>
|
||||
</mat-list-item>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-list-item role="listitem">
|
||||
<span class="console-app__list-key">Address</span>
|
||||
<span class="console-app__list-value">{{ formattedAddress() }}</span>
|
||||
</mat-list-item>
|
||||
</mat-list>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
|
||||
<mat-card appearance="outlined">
|
||||
<mat-card-content>
|
||||
<mat-list role="list">
|
||||
<mat-list-item role="listitem">
|
||||
<h2>Technical Info</h2>
|
||||
</mat-list-item>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-list-item role="listitem">
|
||||
<span class="console-app__list-key">IANA Identifier</span>
|
||||
<span class="console-app__list-value">{{
|
||||
registrarService.registrar()?.ianaIdentifier
|
||||
}}</span>
|
||||
</mat-list-item>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-list-item role="listitem">
|
||||
<div>
|
||||
<span class="console-app__list-key">ICANN Referral Email</span>
|
||||
<span class="console-app__list-value">{{
|
||||
registrarService.registrar()?.icannReferralEmail
|
||||
}}</span>
|
||||
</div>
|
||||
</mat-list-item>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-list-item role="listitem">
|
||||
<span class="console-app__list-key">WHOIS server</span>
|
||||
<span class="console-app__list-value">{{
|
||||
registrarService.registrar()?.whoisServer
|
||||
}}</span>
|
||||
</mat-list-item>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-list-item role="listitem">
|
||||
<span class="console-app__list-key">Referral URL</span>
|
||||
<span class="console-app__list-value">{{
|
||||
registrarService.registrar()?.url
|
||||
}}</span>
|
||||
</mat-list-item>
|
||||
</mat-list>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,59 +1,17 @@
|
||||
// Copyright 2023 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.
|
||||
.console-app__whois {
|
||||
max-width: 616px;
|
||||
|
||||
.settings-whois {
|
||||
margin-top: 1.5rem;
|
||||
&__section {
|
||||
&-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 10px;
|
||||
min-width: 400px;
|
||||
}
|
||||
&__section-address {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 5px;
|
||||
min-width: 400px;
|
||||
width: 50%;
|
||||
max-width: 50%;
|
||||
}
|
||||
&__section-description {
|
||||
display: inline-block;
|
||||
margin-block-start: 1em;
|
||||
width: 160px;
|
||||
}
|
||||
&__section-form {
|
||||
display: inline-block;
|
||||
width: 70%;
|
||||
mat-form-field {
|
||||
width: 90%;
|
||||
min-width: 300px;
|
||||
}
|
||||
input:disabled {
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
&__loading {
|
||||
margin: 2rem 0;
|
||||
}
|
||||
&__actions {
|
||||
margin-top: 50px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-right: 50px;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin: 20px 0;
|
||||
button {
|
||||
margin-left: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
mat-card {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
||||
// 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.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
||||
// 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.
|
||||
@@ -12,13 +12,8 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { Component } from '@angular/core';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import {
|
||||
Registrar,
|
||||
RegistrarService,
|
||||
} from 'src/app/registrar/registrar.service';
|
||||
import { Component, computed } from '@angular/core';
|
||||
import { RegistrarService } from 'src/app/registrar/registrar.service';
|
||||
|
||||
import { WhoisService } from './whois.service';
|
||||
|
||||
@@ -26,51 +21,29 @@ import { WhoisService } from './whois.service';
|
||||
selector: 'app-whois',
|
||||
templateUrl: './whois.component.html',
|
||||
styleUrls: ['./whois.component.scss'],
|
||||
providers: [WhoisService],
|
||||
})
|
||||
export default class WhoisComponent {
|
||||
public static PATH = 'whois';
|
||||
loading = false;
|
||||
inEdit = false;
|
||||
registrar: Registrar;
|
||||
formattedAddress = computed(() => {
|
||||
let result = '';
|
||||
const registrar = this.registrarService.registrar();
|
||||
if (registrar?.localizedAddress?.street) {
|
||||
result += `${registrar?.localizedAddress?.street?.join(' ')} `;
|
||||
}
|
||||
if (registrar?.localizedAddress?.city) {
|
||||
result += `${registrar?.localizedAddress?.city} `;
|
||||
}
|
||||
if (registrar?.localizedAddress?.state) {
|
||||
result += `${registrar?.localizedAddress?.state} `;
|
||||
}
|
||||
if (registrar?.localizedAddress?.street) {
|
||||
result += registrar?.localizedAddress?.countryCode;
|
||||
}
|
||||
return result;
|
||||
});
|
||||
|
||||
constructor(
|
||||
public whoisService: WhoisService,
|
||||
public registrarService: RegistrarService,
|
||||
private _snackBar: MatSnackBar
|
||||
) {
|
||||
this.registrar = JSON.parse(
|
||||
JSON.stringify(this.registrarService.registrar)
|
||||
);
|
||||
}
|
||||
|
||||
enableEdit() {
|
||||
this.inEdit = true;
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.inEdit = false;
|
||||
this.resetDataSource();
|
||||
}
|
||||
|
||||
save() {
|
||||
this.loading = true;
|
||||
this.whoisService.saveChanges(this.registrar).subscribe({
|
||||
complete: () => {
|
||||
this.loading = false;
|
||||
this.resetDataSource();
|
||||
},
|
||||
error: (err: HttpErrorResponse) => {
|
||||
this._snackBar.open(err.error);
|
||||
this.loading = false;
|
||||
},
|
||||
});
|
||||
this.cancel();
|
||||
}
|
||||
|
||||
resetDataSource() {
|
||||
this.registrar = JSON.parse(
|
||||
JSON.stringify(this.registrarService.registrar)
|
||||
);
|
||||
}
|
||||
public registrarService: RegistrarService
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The Nomulus Authors. All Rights Reserved.
|
||||
// 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.
|
||||
@@ -14,21 +14,17 @@
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { switchMap } from 'rxjs';
|
||||
import { Address, RegistrarService } from 'src/app/registrar/registrar.service';
|
||||
import {
|
||||
RegistrarService,
|
||||
WhoisRegistrarFields,
|
||||
} from 'src/app/registrar/registrar.service';
|
||||
import { BackendService } from 'src/app/shared/services/backend.service';
|
||||
|
||||
export interface WhoisRegistrarFields {
|
||||
ianaIdentifier?: number;
|
||||
icannReferralEmail?: string;
|
||||
localizedAddress?: Address;
|
||||
registrarId?: string;
|
||||
url?: string;
|
||||
whoisServer?: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class WhoisService {
|
||||
whoisRegistrarFields: WhoisRegistrarFields = {};
|
||||
editing: boolean = false;
|
||||
|
||||
constructor(
|
||||
private backend: BackendService,
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
<div class="console-app__whois-edit" *ngIf="registrarInEdit">
|
||||
<button
|
||||
mat-icon-button
|
||||
class="console-app__whois-edit-back"
|
||||
aria-label="Back to whois view"
|
||||
(click)="whoisService.editing = false"
|
||||
>
|
||||
<mat-icon>arrow_back</mat-icon>
|
||||
</button>
|
||||
|
||||
<div class="console-app__whois-edit-controls">
|
||||
<span>
|
||||
General registrar information for your WHOIS record. This information is
|
||||
always visible in WHOIS.
|
||||
</span>
|
||||
<div class="spacer"></div>
|
||||
</div>
|
||||
|
||||
<div class="console-app__whois-edit">
|
||||
<h1>Personal info</h1>
|
||||
|
||||
<form (ngSubmit)="save($event)">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Email: </mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="email"
|
||||
[(ngModel)]="registrarInEdit.emailAddress"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
/>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Phone: </mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="text"
|
||||
[(ngModel)]="registrarInEdit.phoneNumber"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
/>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Fax: </mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="text"
|
||||
[(ngModel)]="registrarInEdit.faxNumber"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
/>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Street Address: </mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="text"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
[(ngModel)]="registrarInEdit.localizedAddress.street![1]"
|
||||
/>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>City: </mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="text"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
[(ngModel)]="(registrarInEdit.localizedAddress || {}).city"
|
||||
/>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>State or Province: </mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="text"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
[(ngModel)]="(registrarInEdit.localizedAddress || {}).state"
|
||||
/>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Country: </mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="text"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
[(ngModel)]="(registrarInEdit.localizedAddress || {}).countryCode"
|
||||
/>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Postal code: </mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="text"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
[(ngModel)]="(registrarInEdit.localizedAddress || {}).zip"
|
||||
/>
|
||||
</mat-form-field>
|
||||
|
||||
<h1>Technical info</h1>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>WHOIS server: </mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="text"
|
||||
[(ngModel)]="registrarInEdit.whoisServer"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
/>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Referral URL: </mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="text"
|
||||
[(ngModel)]="registrarInEdit.url"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
/>
|
||||
</mat-form-field>
|
||||
|
||||
<button mat-flat-button color="primary" type="submit">Save</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,19 @@
|
||||
.console-app__whois-edit {
|
||||
max-width: 616px;
|
||||
|
||||
&-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin: 20px 0;
|
||||
button {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
mat-form-field {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// 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, effect } from '@angular/core';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import {
|
||||
Registrar,
|
||||
RegistrarService,
|
||||
} from 'src/app/registrar/registrar.service';
|
||||
import { WhoisService } from './whois.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-whois-edit',
|
||||
templateUrl: './whoisEdit.component.html',
|
||||
styleUrls: ['./whoisEdit.component.scss'],
|
||||
})
|
||||
export default class WhoisEditComponent {
|
||||
registrarInEdit: Registrar | undefined;
|
||||
|
||||
constructor(
|
||||
public whoisService: WhoisService,
|
||||
public registrarService: RegistrarService,
|
||||
private _snackBar: MatSnackBar
|
||||
) {
|
||||
effect(() => {
|
||||
const registrar = this.registrarService.registrar();
|
||||
if (registrar) {
|
||||
this.registrarInEdit = structuredClone(registrar);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
save(e: SubmitEvent) {
|
||||
e.preventDefault();
|
||||
if (!this.registrarInEdit) return;
|
||||
|
||||
this.whoisService.saveChanges(this.registrarInEdit).subscribe({
|
||||
complete: () => {
|
||||
this.whoisService.editing = false;
|
||||
},
|
||||
error: (err: HttpErrorResponse) => {
|
||||
this._snackBar.open(err.error);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user