Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions console-webapp/src/app/shared/services/backend.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,4 +280,15 @@ export class BackendService {
`/console-api/registry-lock-verify?lockVerificationCode=${lockVerificationCode}`
);
}

requestRegistryLockPasswordReset(
registrarId: string,
registryLockEmail: string
) {
return this.http.post('/console-api/password-reset-request', {
type: 'REGISTRY_LOCK',
registrarId,
registryLockEmail,
});
}
}
10 changes: 9 additions & 1 deletion console-webapp/src/app/users/userDetails.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,15 @@ <h2>User details</h2>
roleToDescription(userDetails().role)
}}</span>
</mat-list-item>
@if (userDetails().password) {
@if (userDetails().registryLockEmailAddress) {
<mat-divider></mat-divider>
<mat-list-item role="listitem">
<span class="console-app__list-key">Registry Lock email</span>
<span class="console-app__list-value">{{
userDetails().registryLockEmailAddress
}}</span>
</mat-list-item>
} @if (userDetails().password) {
<mat-divider></mat-divider>
<mat-list-item role="listitem">
<span class="console-app__list-key">Password</span>
Expand Down
3 changes: 3 additions & 0 deletions console-webapp/src/app/users/userDetails.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,8 @@
border: 1px solid #ddd;
border-radius: 10px;
}
.console-app__list-key {
width: 160px;
}
}
}
92 changes: 52 additions & 40 deletions console-webapp/src/app/users/userEditForm.component.html
Original file line number Diff line number Diff line change
@@ -1,45 +1,57 @@
<form (ngSubmit)="saveEdit($event)" #form>
<p *ngIf="isNew()">
<mat-form-field appearance="outline">
<mat-label
>User name prefix:
<mat-icon
matTooltip="Prefix will be combined with registrar ID to create a unique user name - {prefix}.{registrarId}@registry.google"
>help_outline</mat-icon
></mat-label
>
<input
matInput
minlength="3"
maxlength="3"
[required]="true"
[(ngModel)]="user().emailAddress"
[ngModelOptions]="{ standalone: true }"
/>
</mat-form-field>
</p>
<p>
<mat-form-field appearance="outline">
<mat-label
>User Role:
<mat-icon
matTooltip="Viewer role doesn't allow making updates; Editor role allows updates, like Contacts delete or SSL certificate change"
>help_outline</mat-icon
></mat-label
>
<mat-select [(ngModel)]="user().role" name="userRole">
<mat-option value="PRIMARY_CONTACT">Editor</mat-option>
<mat-option value="ACCOUNT_MANAGER">Viewer</mat-option>
</mat-select>
</mat-form-field>
</p>
<div class="console-app__user-edit">
<form (ngSubmit)="saveEdit($event)" #form>
<p *ngIf="isNew()">
<mat-form-field appearance="outline">
<mat-label
>User name prefix:
<mat-icon
matTooltip="Prefix will be combined with registrar ID to create a unique user name - {prefix}.{registrarId}@registry.google"
>help_outline</mat-icon
></mat-label
>
<input
matInput
minlength="3"
maxlength="3"
[required]="true"
[(ngModel)]="user().emailAddress"
[ngModelOptions]="{ standalone: true }"
/>
</mat-form-field>
</p>
<p>
<mat-form-field appearance="outline">
<mat-label
>User Role:
<mat-icon
matTooltip="Viewer role doesn't allow making updates; Editor role allows updates, like Contacts delete or SSL certificate change"
>help_outline</mat-icon
></mat-label
>
<mat-select [(ngModel)]="user().role" name="userRole">
<mat-option value="PRIMARY_CONTACT">Editor</mat-option>
<mat-option value="ACCOUNT_MANAGER">Viewer</mat-option>
</mat-select>
</mat-form-field>
</p>
<button
mat-flat-button
color="primary"
aria-label="Save user"
type="submit"
aria-label="Save changes to the user"
>
Save
</button>
</form>
@if(userDataService.userData()?.isAdmin) {
<button
mat-flat-button
color="primary"
aria-label="Save user"
type="submit"
aria-label="Save changes to the user"
aria-label="Reset registry lock password"
(click)="requestRegistryLockPasswordReset()"
>
Save
Reset registry lock password
</button>
</form>
}
</div>
20 changes: 20 additions & 0 deletions console-webapp/src/app/users/userEditForm.component.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// 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.

.console-app__user-edit {
button {
display: block;
margin-bottom: 5px;
}
}
85 changes: 84 additions & 1 deletion console-webapp/src/app/users/userEditForm.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,56 @@ import {
Component,
ElementRef,
EventEmitter,
Inject,
input,
Output,
ViewChild,
} from '@angular/core';
import { MaterialModule } from '../material.module';
import { FormsModule } from '@angular/forms';
import { User } from './users.service';
import { User, UsersService } from './users.service';
import { UserDataService } from '../shared/services/userData.service';
import { BackendService } from '../shared/services/backend.service';
import { RegistrarService } from '../registrar/registrar.service';
import {
MAT_DIALOG_DATA,
MatDialog,
MatDialogRef,
} from '@angular/material/dialog';
import { filter, switchMap, take } from 'rxjs';
import { MatSnackBar } from '@angular/material/snack-bar';
import { HttpErrorResponse } from '@angular/common/http';

@Component({
selector: 'app-reset-lock-password-dialog',
template: `
<h2 mat-dialog-title>Please confirm the password reset:</h2>
<mat-dialog-content>
This will send a registry lock password reset email to
{{ data.registryLockEmailAddress }}.
</mat-dialog-content>
<mat-dialog-actions>
<button mat-button (click)="onCancel()">Cancel</button>
<button mat-button color="warn" (click)="onSave()">Confirm</button>
</mat-dialog-actions>
`,
imports: [CommonModule, MaterialModule],
})
export class ResetRegistryLockPasswordComponent {
constructor(
public dialogRef: MatDialogRef<ResetRegistryLockPasswordComponent>,
@Inject(MAT_DIALOG_DATA)
public data: { registryLockEmailAddress: string }
) {}

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

onCancel(): void {
this.dialogRef.close(false);
}
}

@Component({
selector: 'app-user-edit-form',
Expand All @@ -39,12 +82,22 @@ export class UserEditFormComponent {
{
emailAddress: '',
role: 'ACCOUNT_MANAGER',
registryLockEmailAddress: '',
},
{ transform: (user: User) => structuredClone(user) }
);

@Output() onEditComplete = new EventEmitter<User>();

constructor(
protected userDataService: UserDataService,
private backendService: BackendService,
private resetRegistryLockPasswordDialog: MatDialog,
private registrarService: RegistrarService,
private usersService: UsersService,
private _snackBar: MatSnackBar
) {}

saveEdit(e: SubmitEvent) {
e.preventDefault();
if (this.form.nativeElement.checkValidity()) {
Expand All @@ -53,4 +106,34 @@ export class UserEditFormComponent {
this.form.nativeElement.reportValidity();
}
}

sendRegistryLockPasswordResetRequest() {
return this.backendService.requestRegistryLockPasswordReset(
this.registrarService.registrarId(),
this.user().registryLockEmailAddress!
);
}

requestRegistryLockPasswordReset() {
const dialogRef = this.resetRegistryLockPasswordDialog.open(
ResetRegistryLockPasswordComponent,
{
data: {
registryLockEmailAddress: this.user().registryLockEmailAddress,
},
}
);
dialogRef
.afterClosed()
.pipe(
take(1),
filter((result) => !!result)
)
.pipe(switchMap((_) => this.sendRegistryLockPasswordResetRequest()))
.subscribe({
next: (_) => this.usersService.currentlyOpenUserEmail.set(''),
error: (err: HttpErrorResponse) =>
this._snackBar.open(err.error || err.message),
});
}
}
1 change: 1 addition & 0 deletions console-webapp/src/app/users/users.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export interface User {
emailAddress: string;
role: string;
password?: string;
registryLockEmailAddress?: string;
}

@Injectable()
Expand Down
35 changes: 17 additions & 18 deletions core/src/main/java/google/registry/model/console/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ public class User extends UpdateAutoTimestampEntity implements Buildable {
@Id @Expose String emailAddress;

/** Optional external email address to use for registry lock confirmation emails. */
@Column String registryLockEmailAddress;
@Column @Expose String registryLockEmailAddress;

/** Roles (which grant permissions) associated with this user. */
@Expose
Expand Down Expand Up @@ -250,51 +250,50 @@ public boolean hasAnyRegistryLockPermission() {
}

@Override
public Builder<? extends User, ?> asBuilder() {
return new Builder<>(clone(this));
public Builder asBuilder() {
return new Builder(clone(this));
}

/** Builder for constructing immutable {@link User} objects. */
public static class Builder<T extends User, B extends Builder<T, B>>
extends GenericBuilder<T, B> {
public static class Builder extends Buildable.Builder<User> {

public Builder() {}

public Builder(T abstractUser) {
super(abstractUser);
public Builder(User user) {
super(user);
}

@Override
public T build() {
public User build() {
checkArgumentNotNull(getInstance().emailAddress, "Email address cannot be null");
checkArgumentNotNull(getInstance().userRoles, "User roles cannot be null");
return super.build();
}

public B setEmailAddress(String emailAddress) {
public Builder setEmailAddress(String emailAddress) {
getInstance().emailAddress = checkValidEmail(emailAddress);
return thisCastToDerived();
return this;
}

public B setRegistryLockEmailAddress(@Nullable String registryLockEmailAddress) {
public Builder setRegistryLockEmailAddress(@Nullable String registryLockEmailAddress) {
getInstance().registryLockEmailAddress =
registryLockEmailAddress == null ? null : checkValidEmail(registryLockEmailAddress);
return thisCastToDerived();
return this;
}

public B setUserRoles(UserRoles userRoles) {
public Builder setUserRoles(UserRoles userRoles) {
checkArgumentNotNull(userRoles, "User roles cannot be null");
getInstance().userRoles = userRoles;
return thisCastToDerived();
return this;
}

public B removeRegistryLockPassword() {
public Builder removeRegistryLockPassword() {
getInstance().registryLockPasswordHash = null;
getInstance().registryLockPasswordSalt = null;
return thisCastToDerived();
return this;
}

public B setRegistryLockPassword(String registryLockPassword) {
public Builder setRegistryLockPassword(String registryLockPassword) {
checkArgument(
getInstance().hasAnyRegistryLockPermission(), "User has no registry lock permission");
checkArgument(
Expand All @@ -304,7 +303,7 @@ public B setRegistryLockPassword(String registryLockPassword) {
byte[] salt = SALT_SUPPLIER.get();
getInstance().registryLockPasswordSalt = base64().encode(salt);
getInstance().registryLockPasswordHash = hashPassword(registryLockPassword, salt);
return thisCastToDerived();
return this;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

import com.google.common.base.Ascii;
import com.google.common.collect.ImmutableList;
import google.registry.model.Buildable.GenericBuilder;
import google.registry.model.Buildable;
import google.registry.model.ImmutableObject;
import google.registry.model.domain.Period;
import google.registry.model.domain.fee.Fee;
Expand Down Expand Up @@ -77,8 +77,7 @@ public String getFeeClass() {
}

/** Builder for {@link FeeCheckResponseExtensionItemCommandV12}. */
public static class Builder
extends GenericBuilder<FeeCheckResponseExtensionItemCommandV12, Builder> {
public static class Builder extends Buildable.Builder<FeeCheckResponseExtensionItemCommandV12> {

public Builder setCommandName(CommandName commandName) {
getInstance().commandName = Ascii.toLowerCase(commandName.name());
Expand Down
Loading