Skip to content

Commit

Permalink
Add suspend / unsuspend to the console
Browse files Browse the repository at this point in the history
  • Loading branch information
ptkach committed Feb 13, 2025
1 parent e78de98 commit d8a4a07
Show file tree
Hide file tree
Showing 8 changed files with 237 additions and 24 deletions.
16 changes: 16 additions & 0 deletions console-webapp/src/app/domains/domainList.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,22 @@ <h1>No domains found</h1>
<mat-icon>key</mat-icon>
<span>Registry Lock</span>
</button>
<button
mat-menu-item
(click)="onSuspendClick(domainName)"
[elementId]="getElementIdForSuspendUnsuspend()"
>
<mat-icon>lock_clock</mat-icon>
<span>Suspend</span>
</button>
<button
mat-menu-item
(click)="onUnsuspendClick(domainName)"
[elementId]="getElementIdForSuspendUnsuspend()"
>
<mat-icon>lock_open</mat-icon>
<span>Unsuspend</span>
</button>
</ng-template>
</mat-menu>
<div
Expand Down
109 changes: 95 additions & 14 deletions console-webapp/src/app/domains/domainList.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,17 @@

import { SelectionModel } from '@angular/cdk/collections';
import { HttpErrorResponse, HttpStatusCode } from '@angular/common/http';
import { Component, ViewChild, effect, Inject } from '@angular/core';
import { Component, effect, Inject, ViewChild } from '@angular/core';
import { MatPaginator, PageEvent } from '@angular/material/paginator';
import { MatSnackBar } from '@angular/material/snack-bar';
import { MatTableDataSource } from '@angular/material/table';
import { Subject, debounceTime, take, filter } from 'rxjs';
import { debounceTime, filter, Subject, take } from 'rxjs';
import { RegistrarService } from '../registrar/registrar.service';
import { Domain, DomainListService } from './domainList.service';
import {
BULK_ACTION_NAMES,
Domain,
DomainListService,
} from './domainList.service';
import { RegistryLockComponent } from './registryLock.component';
import { RegistryLockService } from './registryLock.service';
import {
Expand Down Expand Up @@ -62,6 +66,12 @@ export class ResponseDialogComponent {
}
}

enum Operation {
deleting = 'deleting',
suspending = 'suspending',
unsuspending = 'unsuspending',
}

@Component({
selector: 'app-reason-dialog',
template: `
Expand All @@ -75,23 +85,22 @@ export class ResponseDialogComponent {
</mat-dialog-content>
<mat-dialog-actions>
<button mat-button (click)="onCancel()">Cancel</button>
<button mat-button color="warn" (click)="onDelete()" [disabled]="!reason">
Delete
<button mat-button color="warn" (click)="onSave()" [disabled]="!reason">
Save
</button>
</mat-dialog-actions>
`,
standalone: false,
})
export class ReasonDialogComponent {
reason: string = '';

constructor(
public dialogRef: MatDialogRef<ReasonDialogComponent>,
@Inject(MAT_DIALOG_DATA)
public data: { operation: 'deleting' | 'suspending' }
public data: { operation: Operation }
) {}

onDelete(): void {
onSave(): void {
this.dialogRef.close(this.reason);
}

Expand Down Expand Up @@ -258,6 +267,10 @@ export class DomainListComponent {
return RESTRICTED_ELEMENTS.BULK_DELETE;
}

getElementIdForSuspendUnsuspend() {
return RESTRICTED_ELEMENTS.SUSPEND;
}

getOperationMessage(domain: string) {
if (this.operationResult && this.operationResult[domain])
return this.operationResult[domain].message;
Expand All @@ -267,10 +280,11 @@ export class DomainListComponent {
sendDeleteRequest(reason: string) {
this.isLoading = true;
this.domainListService
.deleteDomains(
this.selection.selected,
.bulkDomainAction(
this.selection.selected.map((d) => d.domainName),
reason,
this.registrarService.registrarId()
this.registrarService.registrarId(),
BULK_ACTION_NAMES.DELETE
)
.pipe(take(1))
.subscribe({
Expand All @@ -294,15 +308,17 @@ export class DomainListComponent {
this.operationResult = result;
this.reloadData();
},
error: (err: HttpErrorResponse) =>
this._snackBar.open(err.error || err.message),
error: (err: HttpErrorResponse) => {
this.isLoading = false;
this._snackBar.open(err.error || err.message);
},
});
}

deleteSelectedDomains() {
const dialogRef = this.dialog.open(ReasonDialogComponent, {
data: {
operation: 'deleting',
operation: Operation.deleting,
},
});

Expand All @@ -314,4 +330,69 @@ export class DomainListComponent {
)
.subscribe(this.sendDeleteRequest.bind(this));
}

sendSuspendUnsuspendRequest(
domainName: string,
reason: string,
isSuspend: boolean
) {
this.isLoading = true;
this.domainListService
.bulkDomainAction(
[domainName],
reason,
this.registrarService.registrarId(),
isSuspend ? BULK_ACTION_NAMES.SUSPEND : BULK_ACTION_NAMES.UNSUSPEND
)
.pipe(take(1))
.subscribe({
next: (result: DomainData) => {
this.isLoading = false;
if (result[domainName].responseCode.toString().startsWith('2')) {
this._snackBar.open(result[domainName].message);
} else {
this.reloadData();
}
},
error: (err: HttpErrorResponse) => {
this.isLoading = false;
this._snackBar.open(err.error || err.message);
},
});
}
onSuspendClick(domainName: string) {
const dialogRef = this.dialog.open(ReasonDialogComponent, {
data: {
operation: Operation.suspending,
},
});

dialogRef
.afterClosed()
.pipe(
take(1),
filter((reason) => !!reason)
)
.subscribe((reason) => {
this.sendSuspendUnsuspendRequest(domainName, reason, true);
});
}

onUnsuspendClick(domainName: string) {
const dialogRef = this.dialog.open(ReasonDialogComponent, {
data: {
operation: Operation.unsuspending,
},
});

dialogRef
.afterClosed()
.pipe(
take(1),
filter((reason) => !!reason)
)
.subscribe((reason) => {
this.sendSuspendUnsuspendRequest(domainName, reason, false);
});
}
}
17 changes: 14 additions & 3 deletions console-webapp/src/app/domains/domainList.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ export interface DomainListResult {
totalResults: number;
}

export enum BULK_ACTION_NAMES {
DELETE = 'DELETE',
SUSPEND = 'SUSPEND',
UNSUSPEND = 'UNSUSPEND',
}

@Injectable({
providedIn: 'root',
})
Expand Down Expand Up @@ -71,11 +77,16 @@ export class DomainListService {
);
}

deleteDomains(domains: Domain[], reason: string, registrarId: string) {
bulkDomainAction(
domains: string[],
reason: string,
registrarId: string,
actionName: BULK_ACTION_NAMES
) {
return this.backendService.bulkDomainAction(
domains.map((d) => d.domainName),
domains,
reason,
'DELETE',
actionName,
registrarId
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export enum RESTRICTED_ELEMENTS {
OTE,
USERS,
BULK_DELETE,
SUSPEND,
}

export const DISABLED_ELEMENTS_PER_ROLE = {
Expand All @@ -28,6 +29,7 @@ export const DISABLED_ELEMENTS_PER_ROLE = {
RESTRICTED_ELEMENTS.OTE,
RESTRICTED_ELEMENTS.USERS,
RESTRICTED_ELEMENTS.BULK_DELETE,
RESTRICTED_ELEMENTS.SUSPEND,
],
SUPPORT_LEAD: [RESTRICTED_ELEMENTS.USERS],
SUPPORT_AGENT: [RESTRICTED_ELEMENTS.USERS],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Copyright 2025 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.

package google.registry.ui.server.console.domains;

import com.google.common.collect.ImmutableMap;
import com.google.gson.JsonElement;
import google.registry.model.console.ConsolePermission;

/** An action that will unsuspend the given domain, removing all 5 server*Prohibited statuses. */
public class ConsoleBulkDomainUnsuspendActionType implements ConsoleDomainActionType {

private static final String DOMAIN_SUSPEND_XML =
"""
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<epp
xmlns="urn:ietf:params:xml:ns:epp-1.0">
<command>
<update>
<domain:update
xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
<domain:name>%DOMAIN_NAME%</domain:name>
<domain:add></domain:add>
<domain:rem>
<domain:status s="serverDeleteProhibited" lang="en"></domain:status>
<domain:status s="serverHold" lang="en"></domain:status>
<domain:status s="serverRenewProhibited" lang="en"></domain:status>
<domain:status s="serverTransferProhibited" lang="en"></domain:status>
<domain:status s="serverUpdateProhibited" lang="en"></domain:status>
</domain:rem>
</domain:update>
</update>
<extension>
<metadata:metadata
xmlns:metadata="urn:google:params:xml:ns:metadata-1.0">
<metadata:reason>Console unsuspension: %REASON%</metadata:reason>
<metadata:requestedByRegistrar>false</metadata:requestedByRegistrar>
</metadata:metadata>
</extension>
<clTRID>RegistryConsole</clTRID>
</command>
</epp>""";

private final String reason;

public ConsoleBulkDomainUnsuspendActionType(JsonElement jsonElement) {
this.reason = jsonElement.getAsJsonObject().get("reason").getAsString();
}

@Override
public String getXmlContentsToRun(String domainName) {
return ConsoleDomainActionType.fillSubstitutions(
DOMAIN_SUSPEND_XML, ImmutableMap.of("DOMAIN_NAME", domainName, "REASON", reason));
}

@Override
public ConsolePermission getNecessaryPermission() {
return ConsolePermission.SUSPEND_DOMAIN;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ public interface ConsoleDomainActionType {

enum BulkAction {
DELETE(ConsoleBulkDomainDeleteActionType.class),
SUSPEND(ConsoleBulkDomainSuspendActionType.class);
SUSPEND(ConsoleBulkDomainSuspendActionType.class),
UNSUSPEND(ConsoleBulkDomainUnsuspendActionType.class);

private final Class<? extends ConsoleDomainActionType> actionClass;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
Expand Down Expand Up @@ -61,6 +62,14 @@ public class ConsoleBulkDomainActionTest {

private static final Gson GSON = GsonUtils.provideGson();

private static ImmutableSet<StatusValue> serverSuspensionStatuses =
ImmutableSet.of(
StatusValue.SERVER_RENEW_PROHIBITED,
StatusValue.SERVER_TRANSFER_PROHIBITED,
StatusValue.SERVER_UPDATE_PROHIBITED,
StatusValue.SERVER_DELETE_PROHIBITED,
StatusValue.SERVER_HOLD);

private final FakeClock clock = new FakeClock(DateTime.parse("2024-05-13T00:00:00.000Z"));

@RegisterExtension
Expand Down Expand Up @@ -135,12 +144,34 @@ void testSuccess_suspend() throws Exception {
"""
{"example.tld":{"message":"Command completed successfully","responseCode":1000}}""");
assertThat(loadByEntity(domain).getStatusValues())
.containsAtLeast(
StatusValue.SERVER_RENEW_PROHIBITED,
StatusValue.SERVER_TRANSFER_PROHIBITED,
StatusValue.SERVER_UPDATE_PROHIBITED,
StatusValue.SERVER_DELETE_PROHIBITED,
StatusValue.SERVER_HOLD);
.containsAtLeastElementsIn(serverSuspensionStatuses);
}

@Test
void testSuccess_unsuspend() throws Exception {
User adminUser =
persistResource(
new User.Builder()
.setEmailAddress("[email protected]")
.setUserRoles(
new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).setIsAdmin(true).build())
.build());
persistResource(domain.asBuilder().addStatusValues(serverSuspensionStatuses).build());
ConsoleBulkDomainAction action =
createAction(
"UNSUSPEND",
GSON.toJsonTree(
ImmutableMap.of("domainList", ImmutableList.of("example.tld"), "reason", "test")),
adminUser);
assertThat(loadByEntity(domain).getStatusValues())
.containsAtLeastElementsIn(serverSuspensionStatuses);
action.run();
assertThat(fakeResponse.getStatus()).isEqualTo(SC_OK);
assertThat(fakeResponse.getPayload())
.isEqualTo(
"""
{"example.tld":{"message":"Command completed successfully","responseCode":1000}}""");
assertThat(loadByEntity(domain).getStatusValues()).containsNoneIn(serverSuspensionStatuses);
}

@Test
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit d8a4a07

Please sign in to comment.