Compare commits

...
Author SHA1 Message Date
Pavlo TkachandGitHub 05b43965d1 Fix console EPP password form, minor adjustments (#2445) 2024-05-17 18:44:44 +00:00
gbrodmanandGitHub 43000a5f80 Refactor common exception handling in ConsoleApiAction (#2443)
There are a bunch of cases where we want common exception handling and
it's annoying to have to deal with the common "set failed response and
make sure to return" a bunch of times.

This allows us to break up request methods more easily, since we can now
often throw exceptions that will break all the way back up to
ConsoleApiAction. Previously, any error handling had to exist in the
primary handler method so it could return.
2024-05-16 18:29:14 +00:00
Pavlo TkachandGitHub a66b9ea749 Add OTE registrars to console /registrars response (#2440) 2024-05-16 14:49:32 +00:00
sarahcaseybotandGitHub 36a660a8ad Remove createBillingCost columns from db schema (#2438) 2024-05-15 18:19:36 +00:00
gbrodmanandGitHub d09bb4ff74 Refactor some registry lock verification code (#2434)
The user, on the front end, should not be required to provide whether or
not they're trying to verify a lock or an unlock. They should only need
the verification code. We can inspect the lock object itself (and the
domain in question) to see whether or not we're verifying a lock or an
unlock.
2024-05-14 16:56:32 +00:00
Lai JiangandGitHub 6ca3cc230f Make logging work correctly on Jetty (#2442) 2024-05-14 14:36:26 +00:00
Pavlo TkachandGitHub 6a5d8ed3b5 Allow console access for all not NONE-type global users (#2441) 2024-05-13 19:24:11 +00:00
Pavlo TkachandGitHub 53dcba1189 Update jetty's console build path (#2439) 2024-05-13 16:08:57 +00:00
Pavlo TkachandGitHub 3e77004274 Create gcp console service (#2433) 2024-05-10 21:29:58 +00:00
gbrodmanandGitHub fd21fcdb84 Add a GetUserCommand (#2435)
This is fairly simple, similar to most of the other Get*Command classes
2024-05-10 21:25:52 +00:00
gbrodmanandGitHub ae14e35df7 Change a few wording bits in the console (#2436)
These probably aren't perfect but they seemed to make sense given the
old console. Nothing major.
2024-05-10 18:27:55 +00:00
Lai JiangandGitHub 94dc9fd0d5 Update GcpJsonFormatter (#2437)
Use the correct JSON field to store the source location info so it can
be parsed by Stackdriver.
2024-05-10 16:47:52 +00:00
gbrodmanandGitHub 7b34659a8f Add registryLockEmailAddress field to User object (#2418)
We've added the field in the database in a previous PR. This is only
used in the old console for now because the new console does not have
registry lock functionality yet
2024-05-09 21:42:45 +00:00
sarahcaseybotandGitHub 808432e709 Remove the createBillingCost field from Tld (#2425)
* Remove the createBillingCost field from Tld

* fix spacing

* Change field name of map

* Rename getter

* Fix formatting

* Fix todo

* unchange column name
2024-05-08 18:14:03 +00:00
Lai JiangandGitHub 73d3b76a89 Remove more usage of AutoValue (#2432)
This PR also removes `SerializedForm` used to serialize
`PendingDeposit`, as it is now a simple record.
2024-05-08 00:50:01 +00:00
Weimin YuandGitHub ca072b4861 Add log traces to Nomulus service on GKE (#2427)
* Add log traces to Nomulus service on GKE

Add request-scope log traces to Nomulus on GKE which, unlike
AppEngine and Cloud Run etc, does not generate traces for hosted
applications. This change only affects the GKE image. It does not affect
the AppEngine services.

Log traces are added to Nomulus-generated logs in request-processing
threads. Forked threads are not covered yet. The single relevant use
case (TimeLimiter) will be addressed in a followup PR.

The main change is in the logging configuration:

*  Use gcp-cloud-logging's LoggingHandler

*  Add gcp-cloud-logging's TraceLoggingEnhancer to the handler.

*  Set a thread-local trace id through the TraceLoggingEnhancer in
   ServletBase on request's entry and clear it on completion.

Also removed an unused class (`RequestLogId`).

* CR

* CR
2024-05-07 19:15:46 +00:00
Pavlo TkachandGitHub 54c896cbb9 Add console epp password integration (#2426) 2024-05-06 18:32:54 +00:00
Pavlo TkachandGitHub 2c7bf2cfdb Update cloudbuild-release.yaml with nokeycheck option (#2431) 2024-05-06 18:01:31 +00:00
gbrodmanandGitHub 49d2e34e12 Add a separate RegistryLock action for the console (#2411)
This handles both GET and POST requests. For POST requests it doesn't
actually change anything about the domains because we will need to add a
verification action (this will be done in a future PR).
2024-05-03 22:37:22 +00:00
Weimin YuandGitHub 5511b41f93 Avoid contention over the RefreshDnsRequest table (#2428)
* Avoid contention over the RefreshDnsRequest table

This table can be small at times, when PSQL may use table scan in
queries by keys. At the SERIALIZABLE isolation level, updates to
unrelated rows may conflict due to this `optimization`.

Lower the isolation level to repeatable read.

* Code review
2024-05-03 20:31:54 +00:00
gbrodmanandGitHub 147cdff555 Add registry lock email address col to UserUpdateHistory (#2424) 2024-05-02 15:51:48 +00:00
Ben McIlwainandGitHub 4b6ade0b14 Bring codebase up to more recent Java standards (#2422)
This includes using the new switch format (though IntelliJ does not yet
understand patterns including default so those aren't used), multiline strings,
replacing some unnecessary type declarations with <>, converting some classes to
records, replacing some Guava predicates with native Java code, and some other
miscellaneous Code Inspection fixes.
2024-05-01 20:48:38 +00:00
Pavlo TkachandGitHub 570618705e Allow console access for FTE globar role (#2419) 2024-05-01 16:19:29 +00:00
sarahcaseybotandGitHub e791608098 Add more indexes to speed up deleteProberDataAction (#2423)
This adds an index on transfer_billing_cancellation_id to Domain and superordinate_domain to Host. When tested on crash with the action limited to only delete 10,000 domains, before these indexes were added the action took about 2 hours to delete 10,000 domains. Once these indexes were added, the action was able to delete the 10,000 domains in a little under 2 minutes.
2024-05-01 15:44:08 +00:00
gbrodmanandGitHub 03b358726a Add Java classes for console history objects (#2350)
This also creates base classes for the objects contained within the
history classes, e.g. RegistrarBase. This is the same way that objects
stored in the HistoryEntry subclasses have base classes, e.g.
DomainBase.
2024-04-30 20:42:40 +00:00
gbrodmanandGitHub d121f8f547 Generate fake XSRF token in FakeConsoleApiParams for tests (#2421) 2024-04-30 17:47:53 +00:00
gbrodmanandGitHub b27218d799 Fix a couple Checkstyle warnings (#2420) 2024-04-29 20:08:53 +00:00
Pavlo TkachandGitHub e78ce42dd5 Add console DUM download (#2402)
* Add console DUM download

* Add console DUM download
2024-04-26 15:56:50 +00:00
Ben McIlwainandGitHub 55fade497d Convert a bunch more @AutoValues to records (#2412) 2024-04-25 16:59:31 +00:00
gbrodmanandGitHub e7501b621a Add DB column for separate rlock email address (#2413)
We cannot rely on the user checking their login email, so we'll want to
send the emails to the other address if configured. This is already the
case in RegistrarPoc.
2024-04-25 15:38:57 +00:00
Weimin YuandGitHub 9c443bede1 Fix conflicts between locks (#2407)
Use REPEATABLE READ for lock acquire/release operation to avoid conlicts
between locks.

Postgresql uses table scan on small tables, causing false sharing at
the SERIALIZABLE isolation level.

See b/333537928 for details.
2024-04-24 18:51:18 +00:00
Lai JiangandGitHub 6d0a746b76 Bind console users to the appropriate IAP roles upon creation (#2403)
Console users need IAP to inject the necessary OIDC tokens into their
request headers and therefore need to be bound to appropriate roles. Note
that in environments managed by latchkey, the bindings will need to be
present in latchkey config files as well, otherwise the changes made by
the nomulus tool will be reverted.

TESTED=ran the nomulus command against alpha and verified that the
bindings are created/removed upon console user creation/deletion.
2024-04-24 15:03:43 +00:00
Pavlo TkachandGitHub 0765e7b209 Console deps update (#2409)
* Update angular/core to 17.3.5

* Update angular/material 17.3.5

* Update angular/cli 17.3.5

* Update angular-eslint 17.3.0

* Disable cli cache

* General console deps update
2024-04-23 19:38:32 +00:00
sarahcaseybotandGitHub f729802094 Make createBillingCostTransitions not null (#2405)
* Make createBillingCostTransitions not null

* Set up createBillingCost field to be removed form config files

* Add clarifying comment
2024-04-23 18:22:45 +00:00
Ben McIlwainandGitHub e809e967a3 Convert more @AutoValues to records, particularly in custom flow classes (#2408) 2024-04-22 20:25:33 +00:00
Pavlo TkachandGitHub 4de2bd5901 Add console backend for EPP password change (#2396) 2024-04-20 10:44:26 +00:00
sarahcaseybotandGitHub b5629ff16f Run deleteProberData cron job daily (#2406)
* Run deleteProberData cron job daily

* Sign the commits

* try signing again
2024-04-19 19:32:14 +00:00
Ben McIlwainandGitHub 91615aef54 Handle bad header names in registrar sheet syncing action (#2404)
The existing behavior was to ignore bad header names, in a way that was
counter-intuitive as a user of the Google Sheet. If a header name was bad (which
could just be someone accidentally changing it not realizing it needs to
correspond exactly to the name of the field on the Java object), then all of the
data in that column was just silently left as-is and never updated. This led to
gradually worsening sync and offset shift errors over time.

Now, it will write out an error message into every single cell in the bad
column, so it's clear that the column name is wrong and does not correspond to any
actual data in the DB.

BUG=http://b/332336068
2024-04-19 17:59:58 +00:00
Ben McIlwainandGitHub fa6898167b Convert more @AutoValues to Java records (#2378) 2024-04-17 19:30:23 +00:00
Lai JiangandGitHub 903b7979de Upgrade to jline 3 (#2400)
jline 3 contains API breaking changes, necessitating changes in
ShellCommand.
2024-04-12 19:57:02 +00:00
Weimin YuandGitHub 8721085d14 Fix BSA validation (#2401)
Unblocked reserved names wrongly reported as missing unblockable domain.
2024-04-12 19:54:59 +00:00
Lai JiangandGitHub e434528cd3 Add nomulus deployment and service manifests (#2389) 2024-04-11 19:01:09 +00:00
Pavlo TkachandGitHub 9ca54e4364 Add UI for EPP Password update (#2393) 2024-04-10 22:29:52 +00:00
Weimin YuandGitHub a16794e2af Run BSA Validate without lock (#2399)
As a read-only action that tolerates staleness, locking is unnecessary.
This should help with the lock contention we are observing.

Also reduces the number of VM instances provisioned for BSA and increase
the idle timeout. This should reduce invocation delay. Longer delay may
cause AppEngine to return `Timeout` status to Cloud Scheduler even
though the cron job succeeds.
2024-04-10 19:58:24 +00:00
Lai JiangandGitHub 496a781572 Upgrade jcommander (#2398) 2024-04-10 17:34:11 +00:00
Ben McIlwainandGitHub 2df583df1a Statically import Truth.assertThat() in tests (#2395)
This also involved breaking out an improperly done assertThat() helper overload
method for JsonObjects into a proper Subject that doesn't further overload
assertThat().
2024-04-09 16:27:26 +00:00
sarahcaseybotandGitHub 4f1ca920a7 Use the createBillingCostTransitions map to get the create cost for a domain (#2390)
* Use the createBillingCostTransitions map to get the create cost for a domain

* Add comment

* Add some TODOs

* use streams to check currency unit
2024-04-05 21:27:55 +00:00
Weimin YuandGitHub 96e33f5b4f Check for missing BSA unblockable domains (#2394)
* Check for missing BSA unblockable domains

All unblockable domains created before the last refresh run should be
reported as unblockable (registered).

All reserved domains that are not registered should be reported as
unblockable (reserved). Note that transient errors may be reported for
newly added reserved domains since we do not maintain update time for
when a reserved label is associated with a TLD. However, this scenario
is extremely rare in operations.

* Addressing review
2024-04-03 00:44:05 +00:00
sarahcaseybotandGitHub dff2d90325 Add batching to DeleteProberDataAction (#2322)
* Add batching to DeleteProberDataAction

* Only get time once

* Add separate query for dry run

* Update querries to actually properly delete all the data

* Fix merge conflicts

* Add test for foreign key constraints

* Make transaction repeatable read

* Make queries to subtables native

* Add native query for GracePeriodHistory

* Kill job after 20 hours

* remove extra time check from read query
2024-03-29 20:51:19 +00:00
sarahcaseybotandGitHub fa344776a1 Drop the should_publish column from ReservedList (#2392) 2024-03-29 16:21:11 +00:00
Pavlo TkachandGitHub eb164809de Add console favicon (#2391) 2024-03-27 20:32:01 +00:00
Pavlo TkachandGitHub 4ddbeb6d06 Add console registrar field focus handler, split whois address field (#2388) 2024-03-27 18:55:57 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
fa53391395 Bump express from 4.19.1 to 4.19.2 in /console-webapp (#2387)
Bumps [express](https://github.com/expressjs/express) from 4.19.1 to 4.19.2.
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/master/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.19.1...4.19.2)

---
updated-dependencies:
- dependency-name: express
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-26 17:28:43 +00:00
sarahcaseybotandGitHub 856e70cf8e Add indexes on domainRepoId to DomainHistoryHost and PollMessage (#2380)
* Add index for domainRepoId to PollMessage and DomainHistoryHost

* Add flyway fix for Concurrent

* fix gradle.properties

* Modify lockfiles

* Update the release tool and add IF NOT EXISTS

* Test removing transactional lock from deploy script

* Add transactional lock flag to actual flyway commands in script

* Remove flag from info command

* Add configuration for integration test
2024-03-26 16:44:14 +00:00
Lai JiangandGitHub 2037611931 Upgrade to Gradle 8.7 (#2386) 2024-03-25 23:57:40 +00:00
Lai JiangandGitHub af1f6e5708 Compile to Java 21 bytecode (#2374)
We have been running in Java 21 runtime for a couple of weeks and every
works as expected.
2024-03-25 13:50:39 +00:00
Weimin YuandGitHub 0df8372407 Change BSA job status notifications (#2385)
Add error notifications for BsaDownload.

Stop sending success notifications.
2024-03-22 19:27:25 +00:00
Pavlo TkachandGitHub 59f4129ee0 Restyle registrar console based on the new design proposal (#2336) 2024-03-21 22:05:09 +00:00
Weimin YuandGitHub de3af34b66 Verify unblockables are truly unblockable (#2381)
* Verify unblockables are truly unblockable

Unblockable domains may become blockable due to deregistration or
removal from the reserved list. The BSA refresh job is responsible
for removing them from the database. This PR verifies that the refreshes
are correct.

Note that recent changes since last refresh are not reflected in the
result, and inconsistency due to recent deregistrations are ignored.
Changes in reserved status or IDN validity are not timestamped,
therefore we cannot ignore recent inconsistencies. However, these
changes are rare.

* Addressing code review

* Addressing code review
2024-03-20 18:52:17 +00:00
Weimin YuandGitHub 5c62dc78ba Fix nom_build command (#2383) 2024-03-20 13:12:59 +00:00
Lai JiangandGitHub 3fdecde6e9 Add a method to test if Nomulus is running on Jetty (#2382) 2024-03-19 15:43:52 +00:00
612 changed files with 19364 additions and 14879 deletions
+2 -5
View File
@@ -47,10 +47,6 @@ war {
if (project.path == ":services:default") {
war {
from("${rootDir}/console-webapp/dist/console-webapp") {
include "**/*"
into("console")
}
from("${coreResourcesDir}/google/registry/ui") {
include "registrar_bin.js"
if (environment != "production") {
@@ -103,10 +99,11 @@ explodeWar.doLast {
file("${it.explodedAppDirectory}/WEB-INF/lib/tools.jar").setWritable(true)
}
appengineDeployAll.mustRunAfter ':console-webapp:deploy'
appengineDeployAll.finalizedBy ':deployCloudSchedulerAndQueue'
rootProject.deploy.dependsOn appengineDeployAll
rootProject.stage.dependsOn appengineStage
tasks['war'].dependsOn ':console-webapp:buildConsoleWebappProd'
tasks['war'].dependsOn ':core:compileProdJS'
tasks['war'].dependsOn ':core:processResources'
tasks['war'].dependsOn ':core:jar'
+2 -4
View File
@@ -342,8 +342,8 @@ subprojects {
// search for `flex-template-base-image` and update the parameter value.
// There are at least two instances, one in core/build.gradle, one in
// release/stage_beam_pipeline.sh
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
project.tasks.test.dependsOn runPresubmits
@@ -539,8 +539,6 @@ task coreDev {
dependsOn 'javadoc'
dependsOn 'checkDependenciesDotGradle'
dependsOn 'checkLicense'
// TODO: @ptkach reenable after console design merged
// dependsOn ':console-webapp:runConsoleWebappUnitTests'
dependsOn ':core:check'
dependsOn 'assemble'
}
+6 -6
View File
@@ -24,7 +24,7 @@ com.google.guava:failureaccess:1.0.2=compileClasspath,deploy_jar,runtimeClasspat
com.google.guava:guava-parent:32.1.1-jre=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
com.google.guava:guava:31.0.1-jre=checkstyle
com.google.guava:guava:32.1.1-jre=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
com.google.guava:guava:33.1.0-jre=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
com.google.guava:guava:33.2.0-jre=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=checkstyle,compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
com.google.inject:guice:5.1.0=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
com.google.j2objc:j2objc-annotations:1.3=checkstyle
@@ -54,11 +54,11 @@ org.jacoco:org.jacoco.ant:0.8.11=jacocoAnt
org.jacoco:org.jacoco.core:0.8.11=jacocoAnt
org.jacoco:org.jacoco.report:0.8.11=jacocoAnt
org.javassist:javassist:3.28.0-GA=checkstyle
org.junit.jupiter:junit-jupiter-api:5.10.2=testCompileClasspath,testRuntimeClasspath
org.junit.jupiter:junit-jupiter-engine:5.10.2=testCompileClasspath,testRuntimeClasspath
org.junit.platform:junit-platform-commons:1.10.2=testCompileClasspath,testRuntimeClasspath
org.junit.platform:junit-platform-engine:1.10.2=testCompileClasspath,testRuntimeClasspath
org.junit:junit-bom:5.10.2=testCompileClasspath,testRuntimeClasspath
org.junit.jupiter:junit-jupiter-api:5.11.0-M1=testCompileClasspath,testRuntimeClasspath
org.junit.jupiter:junit-jupiter-engine:5.11.0-M1=testCompileClasspath,testRuntimeClasspath
org.junit.platform:junit-platform-commons:1.11.0-M1=testCompileClasspath,testRuntimeClasspath
org.junit.platform:junit-platform-engine:1.11.0-M1=testCompileClasspath,testRuntimeClasspath
org.junit:junit-bom:5.11.0-M1=testCompileClasspath,testRuntimeClasspath
org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testRuntimeClasspath
org.ow2.asm:asm-commons:9.6=jacocoAnt
org.ow2.asm:asm-tree:9.6=jacocoAnt
@@ -175,14 +175,7 @@ public class TextDiffSubject extends Subject {
.orElse(0);
}
private static class SideBySideRowFormatter {
private final int maxExpectedLineLength;
private final int maxActualLineLength;
private SideBySideRowFormatter(int maxExpectedLineLength, int maxActualLineLength) {
this.maxExpectedLineLength = maxExpectedLineLength;
this.maxActualLineLength = maxActualLineLength;
}
private record SideBySideRowFormatter(int maxExpectedLineLength, int maxActualLineLength) {
public String formatRow(String expected, String actual, char padChar) {
return String.format(
@@ -262,6 +262,9 @@
{
"moduleLicense": "Unicode/ICU License"
},
{
"moduleLicense": "Unicode-3.0"
},
{
"moduleLicense": "Public Domain",
"moduleName": "aopalliance:aopalliance"
+1 -2
View File
@@ -270,8 +270,7 @@ def generate_gradle_properties() -> str:
def get_root() -> str:
"""Returns the root of the nomulus build tree."""
cur_dir = os.getcwd()
if not os.path.exists(os.path.join(cur_dir, 'buildSrc')) or \
not os.path.exists(os.path.join(cur_dir, 'core')) or \
if not os.path.exists(os.path.join(cur_dir, 'core')) or \
not os.path.exists(os.path.join(cur_dir, 'gradle.properties')):
raise Exception('You must run this script from the root directory')
return cur_dir
+12
View File
@@ -178,6 +178,18 @@ PRESUBMITS = {
{"/node_modules/"},
):
"Do not use shaded dependencies from testcontainers.",
PresubmitCheck(
r".*autovalue\.shaded.*",
"java",
{"/node_modules/"},
):
"Do not use shaded dependencies from autovalue.",
PresubmitCheck(
r".*avro\.shaded.*",
"java",
{"/node_modules/"},
):
"Do not use shaded dependencies from avro.",
PresubmitCheck(
r".*com\.google\.common\.truth\.Truth8.*",
"java",
+4
View File
@@ -36,7 +36,11 @@ yarn-error.log
/libpeerconnection.log
testem.log
/typings
.nx/
# System files
.DS_Store
Thumbs.db
# Build artifact
/staged/dist
+6 -3
View File
@@ -73,10 +73,10 @@
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"browserTarget": "console-webapp:build:production"
"buildTarget": "console-webapp:build:production"
},
"development": {
"browserTarget": "console-webapp:build:development"
"buildTarget": "console-webapp:build:development"
}
},
"defaultConfiguration": "development"
@@ -84,7 +84,7 @@
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "console-webapp:build"
"buildTarget": "console-webapp:build"
}
},
"test": {
@@ -122,6 +122,9 @@
}
},
"cli": {
"cache": {
"enabled": false
},
"analytics": false,
"schematicCollections": [
"@angular-eslint/schematics"
+10 -4
View File
@@ -1,4 +1,4 @@
// Copyright 2022 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.
@@ -11,12 +11,12 @@
// 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.
def consoleDir = "${rootDir}/console-webapp"
def projectParam = "--project=${rootProject.gcpProject}"
clean {
delete "${consoleDir}/node_modules"
delete "${consoleDir}/dist"
delete "${consoleDir}/staged/dist"
}
task npmInstallDeps(type: Exec) {
@@ -62,8 +62,14 @@ task checkFormatting(type: Exec) {
args 'run', 'prettify:check'
}
tasks.runConsoleWebappUnitTests.dependsOn(tasks.npmInstallDeps)
task deploy(type: Exec) {
workingDir "${consoleDir}/staged"
executable 'gcloud'
args 'app', 'deploy', "${projectParam}", '--quiet'
}
tasks.buildConsoleWebappProd.dependsOn(tasks.npmInstallDeps)
tasks.applyFormatting.dependsOn(tasks.npmInstallDeps)
tasks.checkFormatting.dependsOn(tasks.npmInstallDeps)
tasks.build.dependsOn(tasks.checkFormatting)
tasks.deploy.dependsOn(tasks.buildConsoleWebappProd)
+1577 -1792
View File
File diff suppressed because it is too large Load Diff
+23 -23
View File
@@ -4,7 +4,7 @@
"scripts": {
"ng": "ng",
"start": "ng serve --proxy-config dev-proxy.config.json",
"build": "ng build --base-href=/console/",
"build": "ng build --base-href=/console/ --output-path=staged/dist",
"build:local": "ng build --base-href=/default/console/",
"watch": "ng build --watch --configuration development",
"test": "ng test --browsers=ChromeHeadless --watch=false",
@@ -16,35 +16,35 @@
},
"private": true,
"dependencies": {
"@angular/animations": "^17.0.7",
"@angular/cdk": "^17.0.4",
"@angular/common": "^17.0.7",
"@angular/compiler": "^17.0.7",
"@angular/core": "^17.0.7",
"@angular/forms": "^17.0.7",
"@angular/material": "^17.0.4",
"@angular/platform-browser": "^17.0.7",
"@angular/platform-browser-dynamic": "^17.0.7",
"@angular/router": "^17.0.7",
"@angular/animations": "^17.3.5",
"@angular/cdk": "^17.3.5",
"@angular/common": "^17.3.5",
"@angular/compiler": "^17.3.5",
"@angular/core": "^17.3.5",
"@angular/forms": "^17.3.5",
"@angular/material": "^17.3.5",
"@angular/platform-browser": "^17.3.5",
"@angular/platform-browser-dynamic": "^17.3.5",
"@angular/router": "^17.3.5",
"rxjs": "~7.5.0",
"tslib": "^2.3.0",
"zone.js": "~0.14.2"
},
"devDependencies": {
"@angular-devkit/build-angular": "^17.0.7",
"@angular-eslint/builder": "17.1.1",
"@angular-eslint/eslint-plugin": "17.1.1",
"@angular-eslint/eslint-plugin-template": "17.1.1",
"@angular-eslint/schematics": "17.1.1",
"@angular-eslint/template-parser": "17.1.1",
"@angular/cli": "~17.0.7",
"@angular/compiler-cli": "^17.0.7",
"@angular-devkit/build-angular": "^17.3.5",
"@angular-eslint/builder": "17.3.0",
"@angular-eslint/eslint-plugin": "17.3.0",
"@angular-eslint/eslint-plugin-template": "17.3.0",
"@angular-eslint/schematics": "17.3.0",
"@angular-eslint/template-parser": "17.3.0",
"@angular/cli": "~17.3.5",
"@angular/compiler-cli": "^17.3.5",
"@types/jasmine": "~4.0.0",
"@types/node": "^18.11.18",
"@typescript-eslint/eslint-plugin": "^5.59.2",
"@typescript-eslint/parser": "^5.59.2",
"@typescript-eslint/eslint-plugin": "^7.2.0",
"@typescript-eslint/parser": "^7.2.0",
"concurrently": "^7.6.0",
"eslint": "^8.39.0",
"eslint": "^8.57.0",
"jasmine-core": "~4.3.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.1.0",
@@ -54,4 +54,4 @@
"prettier": "2.8.7",
"typescript": "~5.2.2"
}
}
}
+71 -34
View File
@@ -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,69 +13,106 @@
// limitations under the License.
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { TldsComponent } from './tlds/tlds.component';
import { HomeComponent } from './home/home.component';
import { SettingsComponent } from './settings/settings.component';
import SettingsContactComponent from './settings/contact/contact.component';
import SettingsWhoisComponent from './settings/whois/whois.component';
import SettingsUsersComponent from './settings/users/users.component';
import SettingsSecurityComponent from './settings/security/security.component';
import { RegistrarGuard } from './registrar/registrar.guard';
import { RegistrarComponent } from './registrar/registrarsTable.component';
import { EmptyRegistrar } from './registrar/emptyRegistrar.component';
import ContactComponent from './settings/contact/contact.component';
import WhoisComponent from './settings/whois/whois.component';
import SecurityComponent from './settings/security/security.component';
import UsersComponent from './settings/users/users.component';
import { Route, RouterModule } from '@angular/router';
import { BillingInfoComponent } from './billingInfo/billingInfo.component';
import { DomainListComponent } from './domains/domainList.component';
import { HomeComponent } from './home/home.component';
import { RegistrarDetailsComponent } from './registrar/registrarDetails.component';
import { RegistrarComponent } from './registrar/registrarsTable.component';
import { ResourcesComponent } from './resources/resources.component';
import ContactComponent from './settings/contact/contact.component';
import SecurityComponent from './settings/security/security.component';
import { SettingsComponent } from './settings/settings.component';
import UsersComponent from './settings/users/users.component';
import WhoisComponent from './settings/whois/whois.component';
import { SupportComponent } from './support/support.component';
const routes: Routes = [
export interface RouteWithIcon extends Route {
iconName?: string;
}
export const routes: RouteWithIcon[] = [
{ path: '', redirectTo: '/home', pathMatch: 'full' },
{ path: 'registrars', component: RegistrarComponent },
{ path: 'empty-registrar', component: EmptyRegistrar },
{ path: 'home', component: HomeComponent, canActivate: [RegistrarGuard] },
{ path: 'tlds', component: TldsComponent, canActivate: [RegistrarGuard] },
{
path: 'home',
component: HomeComponent,
title: 'Dashboard',
iconName: 'view_comfy_alt',
},
// { path: 'tlds', component: TldsComponent, title: "TLDs", iconName: "event_list" },
{
path: DomainListComponent.PATH,
component: DomainListComponent,
canActivate: [RegistrarGuard],
title: 'Domains',
iconName: 'view_list',
},
{
path: SettingsComponent.PATH,
component: SettingsComponent,
title: 'Settings',
iconName: 'settings',
children: [
{
path: '',
redirectTo: 'registrars',
redirectTo: ContactComponent.PATH,
pathMatch: 'full',
},
{
path: ContactComponent.PATH,
component: SettingsContactComponent,
canActivate: [RegistrarGuard],
component: ContactComponent,
title: 'Contacts',
},
{
path: WhoisComponent.PATH,
component: SettingsWhoisComponent,
canActivate: [RegistrarGuard],
component: WhoisComponent,
title: 'WHOIS Info',
},
{
path: SecurityComponent.PATH,
component: SettingsSecurityComponent,
canActivate: [RegistrarGuard],
component: SecurityComponent,
title: 'Security',
},
{
path: UsersComponent.PATH,
component: SettingsUsersComponent,
canActivate: [RegistrarGuard],
},
{
path: RegistrarComponent.PATH,
component: RegistrarComponent,
component: UsersComponent,
},
],
},
// {
// path: EppConsole.PATH,
// component: EppConsoleComponent,
// title: "EPP Console",
// iconName: "upgrade"
// },
{
path: RegistrarComponent.PATH,
component: RegistrarComponent,
title: 'Registrars',
iconName: 'account_circle',
},
{
path: RegistrarDetailsComponent.PATH,
component: RegistrarDetailsComponent,
},
{
path: BillingInfoComponent.PATH,
component: BillingInfoComponent,
title: 'Billing Info',
iconName: 'credit_card',
},
{
path: ResourcesComponent.PATH,
component: ResourcesComponent,
title: 'Resources',
iconName: 'description',
},
{
path: SupportComponent.PATH,
component: SupportComponent,
title: 'Support',
iconName: 'help',
},
];
@NgModule({
+10 -15
View File
@@ -1,24 +1,19 @@
<div class="console-app">
<app-header (toggleNavOpen)="sidenav.toggle()"></app-header>
<div class="console-app mat-typography">
<app-header (toggleNavOpen)="toggleSidenav()"></app-header>
<mat-sidenav-container class="console-app__container">
<mat-sidenav #sidenav class="console-app__sidebar">
<mat-nav-list>
<a mat-list-item [routerLink]="'/home'" routerLinkActive="active">
Home page
</a>
<a mat-list-item [routerLink]="'/tlds'" routerLinkActive="active">
TLDS
</a>
<a mat-list-item [routerLink]="'/settings'" routerLinkActive="active">
Settings
</a>
</mat-nav-list>
<mat-sidenav
[mode]="breakpointObserver.isMobileView() ? 'over' : 'side'"
[opened]="!breakpointObserver.isMobileView()"
#sidenav
class="console-app__sidebar"
>
<app-navigation />
</mat-sidenav>
<mat-sidenav-content class="console-app__content-wrapper">
<div *ngIf="globalLoader.isLoading" class="console-app__global-spinner">
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
</div>
<div class="console-app__content" *ngIf="renderRouter">
<div class="console-app__content">
<router-outlet></router-outlet>
</div>
</mat-sidenav-content>
+8 -16
View File
@@ -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,8 +13,8 @@
// limitations under the License.
:host {
font-family: Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji",
"Segoe UI Emoji", "Segoe UI Symbol" !important;
font-family: "Google Sans", Roboto, Helvetica, Arial, sans-serif,
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol" !important;
font-size: 14px;
color: #333;
box-sizing: border-box;
@@ -26,26 +26,18 @@
display: flex;
flex-direction: column;
height: 100%;
background: #fff;
&__container {
flex: 1;
margin-top: -12px;
padding-bottom: 36px;
background: transparent;
}
&__sidebar {
min-width: 300px;
a::before {
background-color: transparent;
}
.active {
background-color: var(--secondary);
}
}
&__content-wrapper {
margin: 12px 24px;
min-width: 240px;
border: 0;
}
&__content {
max-width: 1340px;
margin: 0 auto;
padding: 0 16px;
}
&__global-spinner {
margin-bottom: 2rem;
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright 2022 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 -21
View File
@@ -1,4 +1,4 @@
// Copyright 2022 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,12 +12,13 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { AfterViewInit, Component, ViewChild, effect } from '@angular/core';
import { RegistrarService } from './registrar/registrar.service';
import { UserDataService } from './shared/services/userData.service';
import { GlobalLoaderService } from './shared/services/globalLoader.service';
import { NavigationEnd, Router } from '@angular/router';
import { AfterViewInit, Component, ViewChild } from '@angular/core';
import { MatSidenav } from '@angular/material/sidenav';
import { NavigationEnd, Router } from '@angular/router';
import { RegistrarService } from './registrar/registrar.service';
import { BreakPointObserverService } from './shared/services/breakPoint.service';
import { GlobalLoaderService } from './shared/services/globalLoader.service';
import { UserDataService } from './shared/services/userData.service';
@Component({
selector: 'app-root',
@@ -25,32 +26,32 @@ import { MatSidenav } from '@angular/material/sidenav';
styleUrls: ['./app.component.scss'],
})
export class AppComponent implements AfterViewInit {
renderRouter: boolean = true;
@ViewChild('sidenav')
@ViewChild(MatSidenav)
sidenav!: MatSidenav;
constructor(
protected registrarService: RegistrarService,
protected userDataService: UserDataService,
protected globalLoader: GlobalLoaderService,
protected router: Router
) {
effect(() => {
if (registrarService.registrarId()) {
this.renderRouter = false;
setTimeout(() => {
this.renderRouter = true;
}, 400);
}
});
}
protected breakpointObserver: BreakPointObserverService,
private router: Router
) {}
ngAfterViewInit() {
this.router.events.subscribe((event) => {
if (event instanceof NavigationEnd) {
this.sidenav.close();
if (this.breakpointObserver.isMobileView()) {
this.sidenav.close();
}
}
});
}
toggleSidenav() {
if (this.sidenav.opened) {
this.sidenav.close();
} else {
this.sidenav.open();
}
}
}
+40 -41
View File
@@ -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,71 +13,70 @@
// limitations under the License.
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MaterialModule } from './material.module';
import { BackendService } from './shared/services/backend.service';
import { HomeComponent } from './home/home.component';
import { TldsComponent } from './tlds/tlds.component';
import { HeaderComponent } from './header/header.component';
import { SettingsComponent } from './settings/settings.component';
import SettingsContactComponent, {
ContactDetailsDialogComponent,
} from './settings/contact/contact.component';
import { HttpClientModule } from '@angular/common/http';
import { RegistrarComponent } from './registrar/registrarsTable.component';
import { RegistrarGuard } from './registrar/registrar.guard';
import SecurityComponent from './settings/security/security.component';
import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field';
import { EmptyRegistrar } from './registrar/emptyRegistrar.component';
import { RegistrarSelectorComponent } from './registrar/registrarSelector.component';
import { GlobalLoaderService } from './shared/services/globalLoader.service';
import { ContactWidgetComponent } from './home/widgets/contactWidget.component';
import { PromotionsWidgetComponent } from './home/widgets/promotionsWidget.component';
import { TldsWidgetComponent } from './home/widgets/tldsWidget.component';
import { ResourcesWidgetComponent } from './home/widgets/resourcesWidget.component';
import { EppWidgetComponent } from './home/widgets/eppWidget.component';
import { BillingWidgetComponent } from './home/widgets/billingWidget.component';
import { DomainsWidgetComponent } from './home/widgets/domainsWidget.component';
import { SettingsWidgetComponent } from './home/widgets/settingsWidget.component';
import { UserDataService } from './shared/services/userData.service';
import WhoisComponent from './settings/whois/whois.component';
import { SnackBarModule } from './snackbar.module';
import { RegistrarDetailsComponent } from './registrar/registrarDetails.component';
import { BillingInfoComponent } from './billingInfo/billingInfo.component';
import { DomainListComponent } from './domains/domainList.component';
import { DialogBottomSheetWrapper } from './shared/components/dialogBottomSheet.component';
import { HeaderComponent } from './header/header.component';
import { HomeComponent } from './home/home.component';
import { NavigationComponent } from './navigation/navigation.component';
import { RegistrarDetailsComponent } from './registrar/registrarDetails.component';
import { RegistrarSelectorComponent } from './registrar/registrarSelector.component';
import { RegistrarComponent } from './registrar/registrarsTable.component';
import { ResourcesComponent } from './resources/resources.component';
import SettingsContactComponent from './settings/contact/contact.component';
import { ContactDetailsComponent } from './settings/contact/contactDetails.component';
import EppPasswordEditComponent from './settings/security/eppPasswordEdit.component';
import SecurityComponent from './settings/security/security.component';
import SecurityEditComponent from './settings/security/securityEdit.component';
import { SettingsComponent } from './settings/settings.component';
import WhoisComponent from './settings/whois/whois.component';
import WhoisEditComponent from './settings/whois/whoisEdit.component';
import { NotificationsComponent } from './shared/components/notifications/notifications.component';
import { SelectedRegistrarWrapper } from './shared/components/selectedRegistrarWrapper/selectedRegistrarWrapper.component';
import { LocationBackDirective } from './shared/directives/locationBack.directive';
import { BreakPointObserverService } from './shared/services/breakPoint.service';
import { GlobalLoaderService } from './shared/services/globalLoader.service';
import { UserDataService } from './shared/services/userData.service';
import { SnackBarModule } from './snackbar.module';
import { SupportComponent } from './support/support.component';
import { TldsComponent } from './tlds/tlds.component';
@NgModule({
declarations: [
AppComponent,
DialogBottomSheetWrapper,
BillingWidgetComponent,
ContactDetailsDialogComponent,
ContactWidgetComponent,
BillingInfoComponent,
ContactDetailsComponent,
DomainListComponent,
DomainsWidgetComponent,
EmptyRegistrar,
EppWidgetComponent,
EppPasswordEditComponent,
HeaderComponent,
HomeComponent,
PromotionsWidgetComponent,
LocationBackDirective,
NavigationComponent,
NotificationsComponent,
RegistrarComponent,
RegistrarDetailsComponent,
RegistrarSelectorComponent,
ResourcesWidgetComponent,
ResourcesComponent,
SecurityComponent,
SecurityEditComponent,
SelectedRegistrarWrapper,
SettingsComponent,
SettingsContactComponent,
SettingsWidgetComponent,
SupportComponent,
TldsComponent,
TldsWidgetComponent,
WhoisComponent,
WhoisEditComponent,
],
imports: [
AppRoutingModule,
@@ -90,8 +89,8 @@ import { DialogBottomSheetWrapper } from './shared/components/dialogBottomSheet.
],
providers: [
BackendService,
BreakPointObserverService,
GlobalLoaderService,
RegistrarGuard,
UserDataService,
{
provide: MAT_FORM_FIELD_DEFAULT_OPTIONS,
@@ -0,0 +1,16 @@
<app-selected-registrar-wrapper>
<h1 class="mat-headline-4">Billing Info</h1>
<div class="console-app__billing">
<div>
<div class="console-app__billing-subhead">
Billing records and information
</div>
<a class="text-l" href="{{ driveFolderUrl() }}" target="_blank"
>View on Google Drive</a
>
</div>
<div>
<img src="./assets/billing.png" />
</div>
</div>
</app-selected-registrar-wrapper>
@@ -0,0 +1,22 @@
.console-app__billing {
display: flex;
flex-wrap: wrap-reverse;
> div {
flex: 1;
display: flex;
justify-content: center;
flex-direction: column;
text-align: left;
max-width: 300px;
min-width: 200px;
margin-top: -40px;
}
img {
aspect-ratio: 1 / 1;
width: 100%;
}
&-subhead {
font-size: 20px;
margin-bottom: 20px;
}
}
@@ -0,0 +1,47 @@
// 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 { Component, computed } from '@angular/core';
import { RegistrarService } from '../registrar/registrar.service';
import { MatSnackBar } from '@angular/material/snack-bar';
@Component({
selector: 'app-billingInfo',
templateUrl: './billingInfo.component.html',
styleUrls: ['./billingInfo.component.scss'],
})
export class BillingInfoComponent {
public static PATH = 'billingInfo';
constructor(
public registrarService: RegistrarService,
private _snackBar: MatSnackBar
) {}
driveFolderUrl = computed<string>(() => {
if (this.registrarService.registrar()?.driveFolderId) {
return (
'https://drive.google.com/drive/folders/' +
this.registrarService.registrar()?.driveFolderId
);
}
return '';
});
openBillingDetails(e: MouseEvent) {
if (!this.registrarService.registrar()?.driveFolderId) {
e.preventDefault();
this._snackBar.open('Billing Folder ID has not been assigned');
}
}
}
@@ -1,52 +1,76 @@
<div class="console-domains">
<mat-form-field>
<mat-label>Filter</mat-label>
<input
type="search"
matInput
[(ngModel)]="searchTerm"
(ngModelChange)="sendInput()"
#input
/>
</mat-form-field>
<div *ngIf="isLoading; else domains_content" class="console-domains__loading">
<app-selected-registrar-wrapper>
<div class="console-domains">
<h1 class="mat-headline-4">Domains</h1>
@if (totalResults === 0) {
<div class="console-app__empty-domains">
<h1>
<mat-icon class="console-app__empty-domains-icon secondary-text"
>apps_outage</mat-icon
>
</h1>
<h1>No domains found</h1>
</div>
} @else if(isLoading) {
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
</div>
<ng-template #domains_content>
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
} @else {
<a
mat-stroked-button
color="primary"
href="/console-api/dum-download?registrarId={{
registrarService.registrarId()
}}"
class="console-app-domains__download"
>
<mat-icon>download</mat-icon>
Download domains (.csv)
</a>
<mat-form-field class="console-app__domains-filter">
<mat-label>Filter</mat-label>
<input
type="search"
matInput
[(ngModel)]="searchTerm"
(ngModelChange)="sendInput()"
#input
/>
</mat-form-field>
<mat-table
[dataSource]="dataSource"
class="mat-elevation-z0"
class="console-app__domains-table"
>
<ng-container matColumnDef="domainName">
<th mat-header-cell *matHeaderCellDef>Domain Name</th>
<td mat-cell *matCellDef="let element">{{ element.domainName }}</td>
<mat-header-cell *matHeaderCellDef>Domain Name</mat-header-cell>
<mat-cell *matCellDef="let element">{{ element.domainName }}</mat-cell>
</ng-container>
<ng-container matColumnDef="creationTime">
<th mat-header-cell *matHeaderCellDef>Creation Time</th>
<td mat-cell *matCellDef="let element">
<mat-header-cell *matHeaderCellDef>Creation Time</mat-header-cell>
<mat-cell *matCellDef="let element">
{{ element.creationTime.creationTime }}
</td>
</mat-cell>
</ng-container>
<ng-container matColumnDef="registrationExpirationTime">
<th mat-header-cell *matHeaderCellDef>Expiration Time</th>
<td mat-cell *matCellDef="let element">
<mat-header-cell *matHeaderCellDef>Expiration Time</mat-header-cell>
<mat-cell *matCellDef="let element">
{{ element.registrationExpirationTime }}
</td>
</mat-cell>
</ng-container>
<ng-container matColumnDef="statuses">
<th mat-header-cell *matHeaderCellDef>Statuses</th>
<td mat-cell *matCellDef="let element">{{ element.statuses }}</td>
<mat-header-cell *matHeaderCellDef>Statuses</mat-header-cell>
<mat-cell *matCellDef="let element">{{ element.statuses }}</mat-cell>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>
<!-- Row shown when there is no matching data. -->
<tr class="mat-row" *matNoDataRow>
<td class="mat-cell" colspan="4">No domains found</td>
</tr>
</table>
<mat-row *matNoDataRow>
<mat-cell colspan="4">No domains found</mat-cell>
</mat-row>
</mat-table>
<mat-paginator
[length]="totalResults"
[pageIndex]="pageNumber"
@@ -56,5 +80,6 @@
aria-label="Select page of domain results"
showFirstLastButtons
></mat-paginator>
</ng-template>
</div>
}
</div>
</app-selected-registrar-wrapper>
@@ -0,0 +1,41 @@
.console-app {
$min-width: 756px;
&__empty-domains {
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
&-icon {
transform: scale(3);
margin-bottom: 0.5rem;
}
}
&-domains {
position: relative;
&__download {
position: absolute;
top: 0;
right: 0;
}
}
&__domains {
width: 100%;
overflow: auto;
}
&__domains-filter {
min-width: $min-width !important;
width: 100%;
}
&__domains-table {
min-width: $min-width !important;
}
.mat-mdc-paginator {
min-width: $min-width !important;
}
}
@@ -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,15 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, ViewChild } from '@angular/core';
import { MatTableDataSource } from '@angular/material/table';
import { BackendService } from '../shared/services/backend.service';
import { HttpErrorResponse } from '@angular/common/http';
import { Component, ViewChild, effect } from '@angular/core';
import { MatPaginator, PageEvent } from '@angular/material/paginator';
import { RegistrarService } from '../registrar/registrar.service';
import { Domain, DomainListService } from './domainList.service';
import { MatSnackBar } from '@angular/material/snack-bar';
import { MatTableDataSource } from '@angular/material/table';
import { Subject, debounceTime } from 'rxjs';
import { RegistrarService } from '../registrar/registrar.service';
import { BackendService } from '../shared/services/backend.service';
import { Domain, DomainListService } from './domainList.service';
@Component({
selector: 'app-domain-list',
@@ -45,15 +47,22 @@ export class DomainListComponent {
pageNumber?: number;
resultsPerPage = 50;
totalResults?: number;
totalResults?: number = 0;
@ViewChild(MatPaginator, { static: true }) paginator!: MatPaginator;
constructor(
private backendService: BackendService,
private domainListService: DomainListService,
private registrarService: RegistrarService
) {}
protected registrarService: RegistrarService,
private _snackBar: MatSnackBar
) {
effect(() => {
if (this.registrarService.registrarId()) {
this.reloadData();
}
});
}
ngOnInit() {
this.dataSource.paginator = this.paginator;
@@ -63,7 +72,6 @@ export class DomainListComponent {
.subscribe((searchTermValue) => {
this.reloadData();
});
this.reloadData();
}
ngOnDestroy() {
@@ -79,10 +87,16 @@ export class DomainListComponent {
this.totalResults,
this.searchTerm
)
.subscribe((domainListResult) => {
this.dataSource.data = domainListResult.domains;
this.totalResults = domainListResult.totalResults;
this.isLoading = false;
.subscribe({
error: (err: HttpErrorResponse) => {
this._snackBar.open(err.message);
this.isLoading = false;
},
next: (domainListResult) => {
this.dataSource.data = (domainListResult || {}).domains;
this.totalResults = (domainListResult || {}).totalResults || 0;
this.isLoading = false;
},
});
}
@@ -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,9 +13,9 @@
// limitations under the License.
import { Injectable } from '@angular/core';
import { BackendService } from '../shared/services/backend.service';
import { RegistrarService } from '../registrar/registrar.service';
import { tap } from 'rxjs';
import { RegistrarService } from '../registrar/registrar.service';
import { BackendService } from '../shared/services/backend.service';
export interface CreateAutoTimestamp {
creationTime: string;
@@ -61,7 +61,7 @@ export class DomainListService {
)
.pipe(
tap((domainListResult: DomainListResult) => {
this.checkpointTime = domainListResult.checkpointTime;
this.checkpointTime = domainListResult?.checkpointTime;
})
);
}
File diff suppressed because one or more lines are too long
@@ -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.
@@ -16,25 +16,27 @@
&__logo {
color: inherit;
text-decoration: none;
margin-left: -10px;
}
&__menu-btn {
width: 25px;
height: 25px;
padding: 0;
}
&__header {
margin-top: 0;
margin-bottom: 10px;
@media (max-width: 599px) {
.mat-toolbar {
padding: 0;
}
.console-app__logo {
font-size: 16px;
}
button {
padding-left: 0;
padding-right: 0;
width: 30px;
}
margin-bottom: 15px;
.mat-toolbar {
background: transparent;
margin-bottom: 10px;
}
@media (max-width: 480px) {
}
&-user-icon {
margin-left: 20px;
}
}
}
.spacer {
flex: 1;
}
@@ -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.
@@ -13,6 +13,7 @@
// limitations under the License.
import { Component, EventEmitter, Output } from '@angular/core';
import { BreakPointObserverService } from '../shared/services/breakPoint.service';
@Component({
selector: 'app-header',
@@ -22,6 +23,8 @@ import { Component, EventEmitter, Output } from '@angular/core';
export class HeaderComponent {
private isNavOpen = false;
constructor(protected breakpointObserver: BreakPointObserverService) {}
@Output() toggleNavOpen = new EventEmitter<boolean>();
toggleNavPane() {
+49 -10
View File
@@ -1,13 +1,52 @@
<div class="console-app__home">
<h1>Welcome to the Google Registry Console</h1>
<div
class="console-app__home"
[class.console-app__home_tablet]="breakPointObserverService.isTabletView()"
>
<h1 class="mat-headline-4">Dashboard</h1>
<div class="console-app__home-widgets">
<div app-domains-widget class="console-app__widget-wrapper__wide"></div>
<div app-contact-widget class="console-app__widget-wrapper__wide"></div>
<div app-tlds-widget></div>
<div app-promotions-widget class="console-app__widget-wrapper__wide"></div>
<div app-settings-widget class="console-app__widget-wrapper__wide"></div>
<div app-resources-widget></div>
<div app-billing-widget></div>
<div app-epp-widget></div>
<mat-card appearance="outlined">
<mat-card-content>
<h3>
<mat-icon class="secondary-text">view_list</mat-icon>
DUMs
</h3>
<p class="secondary-text">View Domains Under Management</p>
</mat-card-content>
<mat-card-actions>
<button mat-button color="primary" (click)="viewDums()">
View DUMs
</button>
</mat-card-actions>
</mat-card>
<mat-card appearance="outlined">
<mat-card-content>
<h3>
<mat-icon class="secondary-text">settings</mat-icon>
EPP Passwords
</h3>
<p class="secondary-text">View / Update EPP Password</p>
</mat-card-content>
<mat-card-actions>
<button mat-button color="primary" (click)="updateEppPassword()">
Update EPP Password
</button>
</mat-card-actions>
</mat-card>
<mat-card appearance="outlined">
<mat-card-content>
<h3>
<mat-icon class="secondary-text">account_circle</mat-icon>
Registrars
</h3>
<p class="secondary-text">View all registrars</p>
</mat-card-content>
<mat-card-actions>
<button mat-button color="primary" (click)="viewRegistrars()">
Manage Registrars
</button>
</mat-card-actions>
</mat-card>
</div>
</div>
+14 -16
View File
@@ -1,4 +1,4 @@
// Copyright 2022 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,25 +13,23 @@
// limitations under the License.
.console-app {
&__home {
margin-top: 1rem;
}
&__home-widgets {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
grid-gap: 20px;
grid-auto-flow: dense;
display: flex;
gap: 1rem;
h3 {
padding: 0;
margin: 0;
display: flex;
align-items: center;
gap: 10px;
}
mat-card {
height: 100%;
h1 {
margin-top: 1rem;
}
flex: 1;
}
}
@media (max-width: 510px) {
.console-app__widget-wrapper__wide {
grid-column: initial;
&__home_tablet {
.console-app__home-widgets {
flex-direction: column;
}
}
}
@@ -1,4 +1,4 @@
// Copyright 2022 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.
+30 -4
View File
@@ -1,4 +1,4 @@
// Copyright 2022 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,12 +12,38 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, ViewEncapsulation } from '@angular/core';
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { DomainListComponent } from '../domains/domainList.component';
import { RegistrarComponent } from '../registrar/registrarsTable.component';
import SecurityComponent from '../settings/security/security.component';
import { SettingsComponent } from '../settings/settings.component';
import { BreakPointObserverService } from '../shared/services/breakPoint.service';
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.scss'],
encapsulation: ViewEncapsulation.None,
})
export class HomeComponent {}
export class HomeComponent {
constructor(
protected breakPointObserverService: BreakPointObserverService,
private router: Router
) {}
viewRegistrars() {
this.router.navigate([RegistrarComponent.PATH], {
queryParamsHandling: 'merge',
});
}
updateEppPassword() {
this.router.navigate(
[SettingsComponent.PATH + '/' + SecurityComponent.PATH],
{ queryParamsHandling: 'merge' }
);
}
viewDums() {
this.router.navigate([DomainListComponent.PATH], {
queryParamsHandling: 'merge',
});
}
}
@@ -1,22 +0,0 @@
<mat-card>
<mat-card-content>
<div class="console-app__widget">
<a
class="console-app__widget_left"
href="{{ driveFolderUrl }}"
(click)="openBillingDetails($event)"
>
<mat-icon class="console-app__widget-icon">account_balance</mat-icon>
<h1 class="console-app__widget-title">Billing Info</h1>
<h4 class="secondary-text text-center">
<span *ngIf="driveFolderUrl; else noDriveFolderUrl">
View important billing and payments information.
</span>
<ng-template #noDriveFolderUrl>
<span> Your billing folder is pending allocation. </span>
</ng-template>
</h4>
</a>
</div>
</mat-card-content>
</mat-card>
@@ -1,29 +0,0 @@
<mat-card>
<mat-card-content>
<div class="console-app__widget">
<div class="console-app__widget_left">
<mat-icon class="console-app__widget-icon">call</mat-icon>
<h1 class="console-app__widget-title">Contact Support</h1>
<h4 class="secondary-text text-center">
Let us know if you have any questions
</h4>
</div>
<div class="console-app__widget_right">
<div class="console-app__widget-section-header">Give us a Call</div>
<p class="secondary-text">
Call {{ userDataService.userData?.productName }} support at
<a href="tel:{{ userDataService.userData?.supportPhoneNumber }}">{{
userDataService.userData?.supportPhoneNumber
}}</a>
</p>
<div class="console-app__widget-section-header">Send us an Email</div>
<p class="secondary-text">
Email {{ userDataService.userData?.productName }} at
<a href="mailto:{{ userDataService.userData?.supportEmail }}">{{
userDataService.userData?.supportEmail
}}</a>
</p>
</div>
</div>
</mat-card-content>
</mat-card>
@@ -1,30 +0,0 @@
<mat-card>
<mat-card-content>
<div class="console-app__widget">
<div class="console-app__widget_left">
<mat-icon class="console-app__widget-icon">view_list</mat-icon>
<h1 class="console-app__widget-title">Domains</h1>
<h4 class="secondary-text text-center">
Manage domain names and registry lock settings.
</h4>
</div>
<div class="console-app__widget_right">
<button mat-button color="primary" class="console-app__widget-link">
Create a Domain
</button>
<p class="secondary-text">Register a new domain name</p>
<button
mat-button
color="primary"
class="console-app__widget-link"
(click)="openDomainsPage()"
>
View DUMs
</button>
<p class="secondary-text">
Download a csv of all domains under management
</p>
</div>
</div>
</mat-card-content>
</mat-card>
@@ -1,13 +0,0 @@
<mat-card>
<mat-card-content>
<div class="console-app__widget">
<div class="console-app__widget_left">
<mat-icon class="console-app__widget-icon">computer</mat-icon>
<h1 class="console-app__widget-title">EPP Console</h1>
<h4 class="secondary-text text-center">
Test run and execute EPP commands using XML files.
</h4>
</div>
</div>
</mat-card-content>
</mat-card>
@@ -1,27 +0,0 @@
<mat-card class="console-app__widget-wrapper__wide">
<mat-card-content>
<div class="console-app__widget">
<div class="console-app__widget_left">
<mat-icon class="console-app__widget-icon">subject</mat-icon>
<h1 class="console-app__widget-title">Promotions</h1>
<h4 class="secondary-text text-center">
Manage Google Registry promotions and view onboarding process.
</h4>
</div>
<div class="console-app__widget_right">
<button mat-button color="primary" class="console-app__widget-link">
Manage Preferred Partner Program
</button>
<p class="secondary-text">
Onboard or view current preferred partner status
</p>
<button mat-button color="primary" class="console-app__widget-link">
Manage Registry Lock Program
</button>
<p class="secondary-text">
Onboard or view current registry lock status
</p>
</div>
</div>
</mat-card-content>
</mat-card>
@@ -1,17 +0,0 @@
<mat-card>
<mat-card-content>
<div class="console-app__widget">
<a
class="console-app__widget_left"
href="{{ userDataService.userData?.technicalDocsUrl }}"
target="_blank"
>
<mat-icon class="console-app__widget-icon">menu_book</mat-icon>
<h1 class="console-app__widget-title">Resources</h1>
<h4 class="secondary-text text-center">
Use Google Drive to view onboarding FAQs, and technical documentation.
</h4>
</a>
</div>
</mat-card-content>
</mat-card>
@@ -1,53 +0,0 @@
<mat-card class="console-app__widget-wrapper__wide">
<mat-card-content>
<div class="console-app__widget">
<div class="console-app__widget_left">
<mat-icon class="console-app__widget-icon">settings</mat-icon>
<h1 class="console-app__widget-title">Settings</h1>
<h4 class="secondary-text text-center">
Configure registrar settings, manage console users, and view activity
log.
</h4>
</div>
<div class="console-app__widget_right">
<button
mat-button
color="primary"
class="console-app__widget-link"
(click)="openContactsPage()"
>
Contact Information
</button>
<p class="secondary-text">Manage Primary, Technical, etc contacts.</p>
<button
mat-button
color="primary"
class="console-app__widget-link"
(click)="openSecurityPage()"
>
Security
</button>
<p class="secondary-text">
Manage IP allow lists and SSL certificates.
</p>
<button mat-button color="primary" class="console-app__widget-link">
Nomulus Password
</button>
<p class="secondary-text">Reset your Nomulus password.</p>
<button mat-button color="primary" class="console-app__widget-link">
User Management
</button>
<p class="secondary-text">Create and manage console user accounts</p>
<button
mat-button
color="primary"
class="console-app__widget-link"
(click)="openRegistrarsPage()"
>
Registrar Management
</button>
<p class="secondary-text">Create and manage registrar accounts</p>
</div>
</div>
</mat-card-content>
</mat-card>
@@ -1,44 +0,0 @@
// 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.
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { RegistrarComponent } from 'src/app/registrar/registrarsTable.component';
import ContactComponent from 'src/app/settings/contact/contact.component';
import SecurityComponent from 'src/app/settings/security/security.component';
import { SettingsComponent } from 'src/app/settings/settings.component';
@Component({
selector: '[app-settings-widget]',
templateUrl: './settingsWidget.component.html',
})
export class SettingsWidgetComponent {
constructor(private router: Router) {}
openRegistrarsPage() {
this.navigate(RegistrarComponent.PATH);
}
openSecurityPage() {
this.navigate(SecurityComponent.PATH);
}
openContactsPage() {
this.navigate(ContactComponent.PATH);
}
private navigate(route: string) {
this.router.navigate([SettingsComponent.PATH, route]);
}
}
@@ -1,13 +0,0 @@
<mat-card>
<mat-card-content>
<div class="console-app__widget">
<div class="console-app__widget_left">
<mat-icon class="console-app__widget-icon">list_alt</mat-icon>
<h1 class="console-app__widget-title">TLDs</h1>
<h4 class="secondary-text text-center">
View launch phase information for all onboarded and available TLDs.
</h4>
</div>
</div>
</mat-card-content>
</mat-card>
+15 -11
View File
@@ -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,16 +12,23 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { NgModule } from '@angular/core';
import { A11yModule } from '@angular/cdk/a11y';
import { DialogModule } from '@angular/cdk/dialog';
import { CdkMenuModule } from '@angular/cdk/menu';
import { OverlayModule } from '@angular/cdk/overlay';
import { CdkTableModule } from '@angular/cdk/table';
import { CdkTreeModule } from '@angular/cdk/tree';
import { NgModule } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatBadgeModule } from '@angular/material/badge';
import { MatBottomSheetModule } from '@angular/material/bottom-sheet';
import { MatButtonModule } from '@angular/material/button';
import { MatButtonToggleModule } from '@angular/material/button-toggle';
import { MatBottomSheetModule } from '@angular/material/bottom-sheet';
import { MatCardModule } from '@angular/material/card';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatChipsModule } from '@angular/material/chips';
import { MatNativeDateModule, MatRippleModule } from '@angular/material/core';
import { MatDialogModule } from '@angular/material/dialog';
import { MatDividerModule } from '@angular/material/divider';
import { MatGridListModule } from '@angular/material/grid-list';
@@ -29,23 +36,18 @@ import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatListModule } from '@angular/material/list';
import { MatMenuModule } from '@angular/material/menu';
import { MatNativeDateModule, MatRippleModule } from '@angular/material/core';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatRadioModule } from '@angular/material/radio';
import { MatSelectModule } from '@angular/material/select';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { MatTableModule } from '@angular/material/table';
import { MatTabsModule } from '@angular/material/tabs';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatTooltipModule } from '@angular/material/tooltip';
import { MatTreeModule } from '@angular/material/tree';
import { OverlayModule } from '@angular/cdk/overlay';
import { CdkMenuModule } from '@angular/cdk/menu';
import { DialogModule } from '@angular/cdk/dialog';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatChipsModule } from '@angular/material/chips';
@NgModule({
exports: [
@@ -83,6 +85,8 @@ import { MatChipsModule } from '@angular/material/chips';
MatSnackBarModule,
MatPaginatorModule,
MatChipsModule,
MatAutocompleteModule,
ReactiveFormsModule,
],
})
export class MaterialModule {}
@@ -0,0 +1,44 @@
<mat-tree
[dataSource]="dataSource"
[treeControl]="treeControl"
class="console-app__nav-tree"
>
<mat-tree-node
*matTreeNodeDef="let node"
matTreeNodeToggle
(click)="onClick(node)"
[class.active]="router.url.includes(node.path)"
>
<mat-icon class="console-app__nav-icon" *ngIf="node.iconName">
{{ node.iconName }}
</mat-icon>
{{ node.title }}
</mat-tree-node>
<mat-nested-tree-node
*matTreeNodeDef="let node; when: hasChild"
(click)="onClick(node)"
>
<div class="mat-tree-node" [class.active]="router.url.includes(node.path)">
<button
class="console-app__nav-icon_expand"
mat-icon-button
matTreeNodeToggle
[attr.aria-label]="'Toggle ' + node.title"
>
<mat-icon>
{{ treeControl.isExpanded(node) ? "expand_more" : "chevron_right" }}
</mat-icon>
</button>
<mat-icon class="console-app__nav-icon" *ngIf="node.iconName">
{{ node.iconName }}
</mat-icon>
{{ node.title }}
</div>
<div
[class.console-app__nav-tree_invisible]="!treeControl.isExpanded(node)"
role="group"
>
<ng-container matTreeNodeOutlet></ng-container>
</div>
</mat-nested-tree-node>
</mat-tree>
@@ -0,0 +1,71 @@
// 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.
$expand-icon-size: 26px;
.console-app {
&__sidebar {
min-width: 300px;
border: 0;
}
&__nav-icon {
width: $expand-icon-size;
height: $expand-icon-size;
color: var(--secondary) !important;
margin-right: $expand-icon-size;
&_expand {
position: absolute;
left: 0;
margin: auto;
padding: 0px;
width: $expand-icon-size;
height: $expand-icon-size;
}
}
&__nav-tree {
.mat-tree-node {
cursor: pointer;
padding-left: $expand-icon-size;
position: relative;
&:hover {
background-color: var(--light-highlight);
border-radius: 0 25px 25px 0;
}
&.active {
border-radius: 0 25px 25px 0;
background-color: var(--lightest);
}
}
div[role="group"] > .mat-tree-node {
// expand icon + regular icon + spacing = 3 * $expand-icon-size
padding-left: calc($expand-icon-size * 3);
}
ul,
li {
margin-top: 0;
margin-bottom: 0;
list-style-type: none;
}
&_invisible {
display: none;
}
}
}
@@ -0,0 +1,107 @@
// 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 { NestedTreeControl } from '@angular/cdk/tree';
import { Component } from '@angular/core';
import { MatTreeNestedDataSource } from '@angular/material/tree';
import { NavigationEnd, Router } from '@angular/router';
import { Subscription } from 'rxjs';
import { RouteWithIcon, routes } from '../app-routing.module';
interface NavMenuNode extends RouteWithIcon {
parentRoute?: RouteWithIcon;
}
/**
* This component is responsible for rendering navigation menu based on the allowed routes
* and keeping UI in sync when route changes (eg highlights selected route in the menu).
*/
@Component({
selector: 'app-navigation',
templateUrl: './navigation.component.html',
styleUrls: ['./navigation.component.scss'],
})
export class NavigationComponent {
renderRouter: boolean = true;
treeControl = new NestedTreeControl<RouteWithIcon>((node) => node.children);
dataSource = new MatTreeNestedDataSource<RouteWithIcon>();
private subscription!: Subscription;
hasChild = (_: number, node: RouteWithIcon) =>
!!node.children && node.children.length > 0;
constructor(protected router: Router) {
this.dataSource.data = this.ngRoutesToNavMenuNodes(routes);
}
ngOnInit() {
this.subscription = this.router.events.subscribe((navigationParams) => {
if (navigationParams instanceof NavigationEnd) {
this.syncExpandedNavigationWithRoute(navigationParams.url);
}
});
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
syncExpandedNavigationWithRoute(url: string) {
const maybeComponentWithChildren = this.dataSource.data.find((menuNode) => {
return (
// @ts-ignore - optional function added to components with children,
// there's no availble tools to get current active router component
typeof menuNode.component?.matchesUrl === 'function' &&
// @ts-ignore
menuNode.component?.matchesUrl(url)
);
});
if (maybeComponentWithChildren) {
this.treeControl.expand(maybeComponentWithChildren);
}
}
onClick(node: NavMenuNode) {
if (node.parentRoute) {
this.router.navigate([node.parentRoute.path + '/' + node.path], {
queryParamsHandling: 'merge',
});
} else {
this.router.navigate([node.path], { queryParamsHandling: 'merge' });
}
}
/**
* We only want to use routes with titles and we want to provide easy reference to parent node
*/
ngRoutesToNavMenuNodes(routes: RouteWithIcon[]): NavMenuNode[] {
return routes
.filter((r) => r.title)
.map((r) => {
if (r.children) {
return {
...r,
children: r.children
.filter((r) => r.title)
.map((childRoute) => {
return {
...childRoute,
parentRoute: r,
};
}),
};
}
return r;
});
}
}
@@ -1,7 +0,0 @@
<div class="console-app__emty-registrar">
<h1>
<mat-icon class="console-app__emty-registrar-icon">block</mat-icon>
</h1>
<h1>No registrar selected</h1>
<h4 class="mat-body-2">Please select a registrar</h4>
</div>
@@ -1,37 +0,0 @@
// 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.
import { Component, effect } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { RegistrarService } from './registrar.service';
@Component({
selector: 'app-empty-registrar',
templateUrl: './emptyRegistrar.component.html',
styleUrls: ['./emptyRegistrar.component.scss'],
})
export class EmptyRegistrar {
constructor(
private route: ActivatedRoute,
protected registrarService: RegistrarService,
private router: Router
) {
effect(() => {
if (registrarService.registrarId()) {
this.router.navigate([this.route.snapshot.paramMap.get('nextUrl')]);
}
});
}
}
@@ -1,68 +0,0 @@
// 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.
import { TestBed } from '@angular/core/testing';
import { RegistrarGuard } from './registrar.guard';
import {
ActivatedRouteSnapshot,
Router,
RouterStateSnapshot,
} from '@angular/router';
import { RegistrarService } from './registrar.service';
describe('RegistrarGuard', () => {
let guard: RegistrarGuard;
let dummyRegistrarService: RegistrarService;
let routeSpy: Router;
let dummyRoute: RouterStateSnapshot;
beforeEach(() => {
routeSpy = jasmine.createSpyObj<Router>('Router', ['navigate']);
dummyRegistrarService = { activeRegistrarId: '' } as RegistrarService;
dummyRoute = { url: '/value' } as RouterStateSnapshot;
TestBed.configureTestingModule({
providers: [
RegistrarGuard,
{ provide: Router, useValue: routeSpy },
{ provide: RegistrarService, useValue: dummyRegistrarService },
],
});
});
it('should not be able to activate when activeRegistrarId is empty', () => {
guard = TestBed.inject(RegistrarGuard);
const res = guard.canActivate(new ActivatedRouteSnapshot(), dummyRoute);
expect(res).toBeFalsy();
});
it('should be able to activate when activeRegistrarId is not empty', () => {
TestBed.overrideProvider(RegistrarService, {
useValue: { activeRegistrarId: 'value' },
});
guard = TestBed.inject(RegistrarGuard);
const res = guard.canActivate(new ActivatedRouteSnapshot(), dummyRoute);
expect(res).toBeTrue();
});
it('should navigate to empty-registrar screen when activeRegistrarId is empty', () => {
guard = TestBed.inject(RegistrarGuard);
guard.canActivate(new ActivatedRouteSnapshot(), dummyRoute);
expect(routeSpy.navigate).toHaveBeenCalledOnceWith([
'/empty-registrar',
{ nextUrl: dummyRoute.url },
]);
});
});
@@ -1,45 +0,0 @@
// 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.
import { Injectable } from '@angular/core';
import {
ActivatedRouteSnapshot,
Router,
RouterStateSnapshot,
} from '@angular/router';
import { RegistrarService } from './registrar.service';
@Injectable({
providedIn: 'root',
})
export class RegistrarGuard {
constructor(
private router: Router,
private registrarService: RegistrarService
) {}
canActivate(
_: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Promise<boolean> | boolean {
if (this.registrarService.registrarId()) {
return true;
}
return this.router.navigate([
`/empty-registrar`,
{ nextUrl: state.url || '' },
]);
}
}
@@ -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.
@@ -15,44 +15,71 @@
import { Injectable, computed, signal } from '@angular/core';
import { Observable, tap } from 'rxjs';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Router } from '@angular/router';
import { BackendService } from '../shared/services/backend.service';
import {
GlobalLoader,
GlobalLoaderService,
} from '../shared/services/globalLoader.service';
import { MatSnackBar } from '@angular/material/snack-bar';
export interface Address {
street?: string[];
city?: string;
countryCode?: string;
zip?: string;
state?: string;
export interface IpAllowListItem {
value: string;
}
export interface Registrar {
export interface Address {
city?: string;
countryCode?: string;
state?: string;
street?: string[];
zip?: string;
}
export interface SecuritySettingsBackendModel {
clientCertificate?: string;
failoverClientCertificate?: string;
ipAddressAllowList?: Array<string>;
// TODO: @ptkach At some point we want to add a back-end support for this
eppPasswordLastUpdated?: string;
}
export interface SecuritySettings
extends Omit<SecuritySettingsBackendModel, 'ipAddressAllowList'> {
ipAddressAllowList?: Array<IpAllowListItem>;
}
export interface WhoisRegistrarFields {
ianaIdentifier?: number;
icannReferralEmail: string;
localizedAddress: Address;
registrarId: string;
url: string;
whoisServer: string;
}
export interface Registrar
extends WhoisRegistrarFields,
SecuritySettingsBackendModel {
allowedTlds?: string[];
billingAccountMap?: object;
driveFolderId?: string;
emailAddress?: string;
faxNumber?: string;
ianaIdentifier?: number;
icannReferralEmail?: string;
ipAddressAllowList?: string[];
localizedAddress?: Address;
phoneNumber?: string;
registrarId: string;
registrarName: string;
registryLockAllowed?: boolean;
url?: string;
whoisServer?: string;
}
@Injectable({
providedIn: 'root',
})
export class RegistrarService implements GlobalLoader {
registrarId = signal<string>('');
registrarId = signal<string>(
new URLSearchParams(document.location.hash.split('?')[1]).get(
'registrarId'
) || ''
);
registrars = signal<Registrar[]>([]);
registrar = computed<Registrar | undefined>(() =>
this.registrars().find((r) => r.registrarId === this.registrarId())
@@ -61,7 +88,8 @@ export class RegistrarService implements GlobalLoader {
constructor(
private backend: BackendService,
private globalLoader: GlobalLoaderService,
private _snackBar: MatSnackBar
private _snackBar: MatSnackBar,
private router: Router
) {
this.loadRegistrars().subscribe((r) => {
this.globalLoader.stopGlobalLoader(this);
@@ -70,7 +98,14 @@ export class RegistrarService implements GlobalLoader {
}
public updateSelectedRegistrar(registrarId: string) {
this.registrarId.set(registrarId);
if (registrarId !== this.registrarId()) {
this.registrarId.set(registrarId);
// add registrarId to url query params, so that we can pick it up after page refresh
this.router.navigate([], {
queryParams: { registrarId },
queryParamsHandling: 'merge',
});
}
}
public loadRegistrars(): Observable<Registrar[]> {
@@ -83,6 +118,23 @@ export class RegistrarService implements GlobalLoader {
);
}
saveRegistrar(registrar: Registrar) {
return this.backend.postRegistrar(registrar).pipe(
tap((registrar) => {
if (registrar) {
this.registrars.set(
this.registrars().map((r) => {
if (r.registrarId === registrar.registrarId) {
return registrar;
}
return r;
})
);
}
})
);
}
loadingTimeout() {
this._snackBar.open('Timeout loading registrars');
}
@@ -1,40 +1,99 @@
<div class="registrarDetails" *ngIf="registrarInEdit">
<h3 mat-dialog-title>Edit Registrar: {{ registrarInEdit.registrarId }}</h3>
<div mat-dialog-content>
<div class="console-app__registrar-view">
<h1 class="mat-headline-4">Registrars</h1>
<mat-divider></mat-divider>
<div class="console-app__registrar-view-content">
<div class="console-app__registrar-view-controls">
<button mat-icon-button aria-label="Back to registrars list" backButton>
<mat-icon>arrow_back</mat-icon>
</button>
<div class="spacer"></div>
@if(!inEdit && !registrarNotFound) {
<button
mat-flat-button
color="primary"
aria-label="Edit Registrar"
(click)="inEdit = true"
>
<mat-icon>edit</mat-icon>
Edit
</button>
<button mat-icon-button aria-label="Delete Registrar">
<mat-icon>delete</mat-icon>
</button>
}
</div>
@if(registrarNotFound) {
<h1>Registrar not found</h1>
} @else {
<h1>{{ registrarInEdit.registrarId }}</h1>
<h2 *ngIf="registrarInEdit.registrarName !== registrarInEdit.registrarId">
{{ registrarInEdit.registrarName }}
</h2>
@if(inEdit) {
<form (ngSubmit)="saveAndClose()">
<mat-form-field class="registrarDetails__input">
<mat-label>Registry Lock:</mat-label>
<mat-select
[(ngModel)]="registrarInEdit.registryLockAllowed"
name="registryLockAllowed"
>
<mat-option [value]="true">True</mat-option>
<mat-option [value]="false">False</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="registrarDetails__input">
<mat-label>Onboarded TLDs: </mat-label>
<mat-chip-grid #chipGrid aria-label="Enter TLD">
<mat-chip-row
*ngFor="let tld of registrarInEdit.allowedTlds"
(removed)="removeTLD(tld)"
<div>
<mat-form-field appearance="outline">
<mat-label>Registry Lock:</mat-label>
<mat-select
[(ngModel)]="registrarInEdit.registryLockAllowed"
name="registryLockAllowed"
>
{{ tld }}
<button matChipRemove aria-label="'remove ' + tld">
<mat-icon>cancel</mat-icon>
</button>
</mat-chip-row>
</mat-chip-grid>
<input
placeholder="New tld..."
[matChipInputFor]="chipGrid"
(matChipInputTokenEnd)="addTLD($event)"
/>
</mat-form-field>
<mat-dialog-actions>
<button mat-button (click)="this.params?.close()">Cancel</button>
<button type="submit" mat-button color="primary">Save</button>
</mat-dialog-actions>
<mat-option [value]="true">True</mat-option>
<mat-option [value]="false">False</mat-option>
</mat-select>
</mat-form-field>
</div>
<div>
<mat-form-field appearance="outline">
<mat-label>Onboarded TLDs: </mat-label>
<mat-chip-grid #chipGrid aria-label="Enter TLD">
<mat-chip-row
*ngFor="let tld of registrarInEdit.allowedTlds"
(removed)="removeTLD(tld)"
>
{{ tld }}
<button matChipRemove aria-label="'remove ' + tld">
<mat-icon>cancel</mat-icon>
</button>
</mat-chip-row>
</mat-chip-grid>
<input
placeholder="New tld..."
[matChipInputFor]="chipGrid"
(matChipInputTokenEnd)="addTLD($event)"
/>
</mat-form-field>
</div>
<button
mat-flat-button
color="primary"
aria-label="Edit Registrar"
type="submit"
>
Save
</button>
</form>
} @else {
<mat-card appearance="outlined">
<mat-card-content>
<mat-list role="list">
<mat-list-item role="listitem">
<h2>Registrar details</h2>
</mat-list-item>
<mat-divider></mat-divider>
@for (column of columns; track column.columnDef) {
<mat-list-item role="listitem">
<span class="console-app__list-key">{{ column.header }} </span>
<span
class="console-app__list-value"
[innerHTML]="column.cell(registrarInEdit).replace('<br/>', ' ')"
></span>
</mat-list-item>
<mat-divider></mat-divider>
}
</mat-list>
</mat-card-content>
</mat-card>
} }
</div>
</div>
@@ -1,8 +1,19 @@
.registrarDetails {
min-width: 30vw;
&__input {
display: block;
margin-top: 0.5rem;
.console-app {
&__registrar-view {
&-controls {
display: flex;
align-items: center;
margin: 20px 0;
}
}
&__registrar-view-content {
max-width: 616px;
mat-divider:last-child {
display: none;
}
mat-form-field {
margin-bottom: 20px;
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.
@@ -12,38 +12,51 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component } from '@angular/core';
import { Registrar, RegistrarService } from './registrar.service';
import { HttpErrorResponse } from '@angular/common/http';
import { Component, OnInit } from '@angular/core';
import { MatChipInputEvent } from '@angular/material/chips';
import { DialogBottomSheetContent } from '../shared/components/dialogBottomSheet.component';
type RegistrarDetailsParams = {
close: Function;
data: {
registrar: Registrar;
};
};
import { MatSnackBar } from '@angular/material/snack-bar';
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
import { Subscription } from 'rxjs';
import { Registrar, RegistrarService } from './registrar.service';
import { RegistrarComponent, columns } from './registrarsTable.component';
@Component({
selector: 'app-registrar-details',
templateUrl: './registrarDetails.component.html',
styleUrls: ['./registrarDetails.component.scss'],
})
export class RegistrarDetailsComponent implements DialogBottomSheetContent {
export class RegistrarDetailsComponent implements OnInit {
public static PATH = 'registrars/:id';
inEdit: boolean = false;
registrarInEdit!: Registrar;
params?: RegistrarDetailsParams;
registrarNotFound: boolean = false;
columns = columns.filter((c) => !c.hiddenOnDetailsCard);
private subscription!: Subscription;
constructor(protected registrarService: RegistrarService) {}
constructor(
protected registrarService: RegistrarService,
private route: ActivatedRoute,
private _snackBar: MatSnackBar,
private router: Router
) {}
init(params: RegistrarDetailsParams) {
this.params = params;
this.registrarInEdit = JSON.parse(
JSON.stringify(this.params.data.registrar)
);
}
saveAndClose() {
this.params?.close();
ngOnInit(): void {
this.subscription = this.route.paramMap.subscribe((params: ParamMap) => {
this.registrarInEdit = structuredClone(
this.registrarService
.registrars()
.filter((r) => r.registrarId === params.get('id'))[0]
);
if (!this.registrarInEdit) {
this._snackBar.open(
`Registrar with id ${params.get('id')} is not available`
);
this.registrarNotFound = true;
} else {
this.registrarNotFound = false;
}
});
}
addTLD(e: MatChipInputEvent) {
@@ -58,4 +71,22 @@ export class RegistrarDetailsComponent implements DialogBottomSheetContent {
(v) => v != tld
);
}
saveAndClose() {
this.registrarService.saveRegistrar(this.registrarInEdit).subscribe({
complete: () => {
this.router.navigate([RegistrarComponent.PATH], {
queryParamsHandling: 'merge',
});
},
error: (err: HttpErrorResponse) => {
this._snackBar.open(err.error);
},
});
this.inEdit = false;
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
}
@@ -1,29 +1,33 @@
<div class="console-app__registrar">
<button
mat-button
[routerLink]="'/settings/registrars'"
routerLinkActive="active"
*ngIf="isMobile; else desktop"
<mat-form-field
class="example-full-width"
class="mat-form-field-density-5"
appearance="outline"
>
{{ registrarService.registrarId() || "Select registrar" }}
<mat-icon>open_in_new</mat-icon>
</button>
<ng-template #desktop>
<mat-form-field class="mat-form-field-density-5" appearance="fill">
<mat-label>Registrar</mat-label>
<mat-select
[ngModel]="registrarService.registrarId()"
(selectionChange)="
registrarService.updateSelectedRegistrar($event.value)
"
<mat-label>Registrar</mat-label>
<input
type="text"
placeholder=""
aria-label="Select Registrar"
matInput
[ngModel]="registrarInput()"
(ngModelChange)="registrarInput.set($event)"
[ngModelOptions]="{ standalone: true }"
(focus)="onFocus()"
[matAutocomplete]="auto"
/>
<mat-autocomplete
autoActiveFirstOption
#auto="matAutocomplete"
(optionSelected)="onSelect($event.option.value)"
>
@for (registrarId of filteredOptions; track registrarId) {
<mat-option
[value]="registrarId"
selected="registrarId === registrarService.registrarId()"
>{{ registrarId }}</mat-option
>
<mat-option
*ngFor="let registrar of registrarService.registrars()"
[value]="registrar.registrarId"
>
{{ registrar.registrarId }}
</mat-option>
</mat-select>
</mat-form-field>
</ng-template>
}
</mat-autocomplete>
</mat-form-field>
</div>
@@ -3,16 +3,5 @@
display: flex;
justify-content: center;
align-items: baseline;
// Fix for angular v15 label issue
// https://github.com/angular/components/issues/26579
.mat-mdc-floating-label {
display: inline !important;
}
.mat-mdc-select-arrow-wrapper {
transform: translateY(0) !important;
}
}
&__title {
margin-right: 1rem;
}
}
@@ -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,35 +12,36 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit } from '@angular/core';
import { Component, computed, effect, signal } from '@angular/core';
import { RegistrarService } from './registrar.service';
import { BreakpointObserver } from '@angular/cdk/layout';
import { distinctUntilChanged } from 'rxjs';
const MOBILE_LAYOUT_BREAKPOINT = '(max-width: 599px)';
@Component({
selector: 'app-registrar-selector',
templateUrl: './registrarSelector.component.html',
styleUrls: ['./registrarSelector.component.scss'],
})
export class RegistrarSelectorComponent implements OnInit {
protected isMobile: boolean = false;
export class RegistrarSelectorComponent {
registrarInput = signal<string>(this.registrarService.registrarId());
filteredOptions?: string[];
allRegistrarIds = computed(() =>
this.registrarService.registrars().map((r) => r.registrarId)
);
readonly breakpoint$ = this.breakpointObserver
.observe([MOBILE_LAYOUT_BREAKPOINT])
.pipe(distinctUntilChanged());
constructor(
protected registrarService: RegistrarService,
protected breakpointObserver: BreakpointObserver
) {}
ngOnInit(): void {
this.breakpoint$.subscribe(() => this.breakpointChanged());
constructor(protected registrarService: RegistrarService) {
effect(() => {
const filterValue = this.registrarInput().toLowerCase();
this.filteredOptions = this.allRegistrarIds().filter((option) =>
option.toLowerCase().includes(filterValue)
);
});
}
private breakpointChanged() {
this.isMobile = this.breakpointObserver.isMatched(MOBILE_LAYOUT_BREAKPOINT);
onFocus() {
// We reset the list of options after selection, so that user doesn't have to clear it out
this.filteredOptions = this.allRegistrarIds();
}
onSelect(registrarId: string) {
this.registrarService.updateSelectedRegistrar(registrarId);
}
}
@@ -1,4 +1,5 @@
<div class="console-app__registrars">
<h1 class="mat-headline-4">Registrars</h1>
<mat-form-field class="console-app__registrars-filter">
<mat-label>Search</mat-label>
<input
@@ -11,24 +12,10 @@
</mat-form-field>
<mat-table
[dataSource]="dataSource"
class="mat-elevation-z8"
class="mat-elevation-z0"
class="console-app__registrars-table"
matSort
>
<ng-container matColumnDef="edit">
<mat-header-cell *matHeaderCellDef></mat-header-cell>
<mat-cell *matCellDef="let row">
<button
mat-icon-button
color="primary"
aria-label="Edit registrar"
(click)="openDetails($event, row)"
>
<mat-icon>edit</mat-icon>
</button>
</mat-cell>
</ng-container>
<ng-container
*ngFor="let column of columns"
[matColumnDef]="column.columnDef"
@@ -39,16 +26,13 @@
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row
*matRowDef="let row; columns: displayedColumns"
(click)="registrarService.updateSelectedRegistrar(row.registrarId)"
(click)="openDetails(row.registrarId)"
></mat-row>
</mat-table>
<mat-paginator
class="mat-elevation-z8"
class="mat-elevation-z0"
[pageSizeOptions]="[5, 10, 20]"
showFirstLastButtons
></mat-paginator>
<app-dialog-bottom-sheet-wrapper
#registrarDetailsView
></app-dialog-bottom-sheet-wrapper>
</div>
@@ -2,7 +2,6 @@
$min-width: 756px;
&__registrars {
margin-top: 1.5rem;
width: 100%;
overflow: auto;
}
@@ -27,7 +26,7 @@
}
&-driveId {
min-width: 200px;
word-break: break-all;
word-break: break-word;
}
&-registryLockAllowed {
max-width: 80px;
@@ -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.
@@ -13,12 +13,60 @@
// limitations under the License.
import { Component, ViewChild, ViewEncapsulation } from '@angular/core';
import { Registrar, RegistrarService } from './registrar.service';
import { MatPaginator } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { RegistrarDetailsComponent } from './registrarDetails.component';
import { DialogBottomSheetWrapper } from '../shared/components/dialogBottomSheet.component';
import { Router } from '@angular/router';
import { Registrar, RegistrarService } from './registrar.service';
export const columns = [
{
columnDef: 'registrarId',
header: 'Registrar Id',
cell: (record: Registrar) => `${record.registrarId || ''}`,
hiddenOnDetailsCard: true,
},
{
columnDef: 'registrarName',
header: 'Name',
cell: (record: Registrar) => `${record.registrarName || ''}`,
hiddenOnDetailsCard: true,
},
{
columnDef: 'allowedTlds',
header: 'TLDs',
cell: (record: Registrar) => `${(record.allowedTlds || []).join(', ')}`,
},
{
columnDef: 'emailAddress',
header: 'Username',
cell: (record: Registrar) => `${record.emailAddress || ''}`,
},
{
columnDef: 'ianaIdentifier',
header: 'IANA ID',
cell: (record: Registrar) => `${record.ianaIdentifier || ''}`,
},
{
columnDef: 'billingAccountMap',
header: 'Billing Accounts',
cell: (record: Registrar) =>
// @ts-ignore - completely legit line, but TS keeps complaining
`${Object.entries(record.billingAccountMap).reduce((acc, [key, val]) => {
return `${acc}${key}=${val}<br/>`;
}, '')}`,
},
{
columnDef: 'registryLockAllowed',
header: 'Registry Lock',
cell: (record: Registrar) => `${record.registryLockAllowed}`,
},
{
columnDef: 'driveId',
header: 'Drive ID',
cell: (record: Registrar) => `${record.driveFolderId || ''}`,
},
];
@Component({
selector: 'app-registrar',
@@ -29,63 +77,17 @@ import { DialogBottomSheetWrapper } from '../shared/components/dialogBottomSheet
export class RegistrarComponent {
public static PATH = 'registrars';
dataSource: MatTableDataSource<Registrar>;
columns = [
{
columnDef: 'registrarId',
header: 'Registrar Id',
cell: (record: Registrar) => `${record.registrarId || ''}`,
},
{
columnDef: 'registrarName',
header: 'Name',
cell: (record: Registrar) => `${record.registrarName || ''}`,
},
{
columnDef: 'allowedTlds',
header: 'TLDs',
cell: (record: Registrar) => `${(record.allowedTlds || []).join(', ')}`,
},
{
columnDef: 'emailAddress',
header: 'Username',
cell: (record: Registrar) => `${record.emailAddress || ''}`,
},
{
columnDef: 'ianaIdentifier',
header: 'IANA ID',
cell: (record: Registrar) => `${record.ianaIdentifier || ''}`,
},
{
columnDef: 'billingAccountMap',
header: 'Billing Accounts',
cell: (record: Registrar) =>
// @ts-ignore - completely legit line, but TS keeps complaining
`${Object.entries(record.billingAccountMap).reduce(
(acc, [key, val]) => {
return `${acc}${key}=${val}<br/>`;
},
''
)}`,
},
{
columnDef: 'registryLockAllowed',
header: 'Registry Lock',
cell: (record: Registrar) => `${record.registryLockAllowed}`,
},
{
columnDef: 'driveId',
header: 'Drive ID',
cell: (record: Registrar) => `${record.driveFolderId || ''}`,
},
];
displayedColumns = ['edit'].concat(this.columns.map((c) => c.columnDef));
columns = columns;
displayedColumns = this.columns.map((c) => c.columnDef);
@ViewChild(MatPaginator) paginator!: MatPaginator;
@ViewChild(MatSort) sort!: MatSort;
@ViewChild('registrarDetailsView')
detailsComponentWrapper!: DialogBottomSheetWrapper;
constructor(protected registrarService: RegistrarService) {
constructor(
protected registrarService: RegistrarService,
private router: Router
) {
this.dataSource = new MatTableDataSource<Registrar>(
registrarService.registrars()
);
@@ -96,16 +98,15 @@ export class RegistrarComponent {
this.dataSource.sort = this.sort;
}
openDetails(event: MouseEvent, registrar: Registrar) {
event.stopPropagation();
this.detailsComponentWrapper.open<RegistrarDetailsComponent>(
RegistrarDetailsComponent,
{ registrar }
);
openDetails(registrarId: string) {
this.router.navigate(['registrars/', registrarId], {
queryParamsHandling: 'merge',
});
}
applyFilter(event: Event) {
const filterValue = (event.target as HTMLInputElement).value;
// TODO: consider filteing out only by registrar name
this.dataSource.filter = filterValue.trim().toLowerCase();
}
}
@@ -0,0 +1,16 @@
<h1 class="mat-headline-4">Resources</h1>
<div class="console-app__resources">
<div>
<div class="console-app__resources-subhead">Technical resources</div>
<a
class="text-l"
href="{{ userDataService.userData.technicalDocsUrl }}"
target="_blank"
>View onboarding FAQs, TLD information, and technical documentation on
Google Drive</a
>
</div>
<div>
<img src="./assets/resources.png" />
</div>
</div>
@@ -0,0 +1,22 @@
.console-app__resources {
display: flex;
flex-wrap: wrap-reverse;
> div {
flex: 1;
display: flex;
justify-content: center;
flex-direction: column;
text-align: left;
max-width: 300px;
min-width: 200px;
margin-top: 30px;
}
img {
aspect-ratio: 1 / 1;
width: 100%;
}
&-subhead {
font-size: 20px;
margin-bottom: 20px;
}
}
@@ -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,12 +13,14 @@
// limitations under the License.
import { Component } from '@angular/core';
import { UserDataService } from 'src/app/shared/services/userData.service';
import { UserDataService } from '../shared/services/userData.service';
@Component({
selector: '[app-resources-widget]',
templateUrl: './resourcesWidget.component.html',
selector: 'app-resources',
templateUrl: './resources.component.html',
styleUrls: ['./resources.component.scss'],
})
export class ResourcesWidgetComponent {
constructor(public userDataService: UserDataService) {}
export class ResourcesComponent {
public static PATH = 'resources';
constructor(protected userDataService: UserDataService) {}
}
@@ -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,22 @@
// 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;
}
.contact__name-column-title {
color: #5f6368;
font-weight: 500;
padding: 10px 0;
}
.contact__name-column-roles {
margin-bottom: 10px;
}
}
&__empty-contacts {
display: flex;
@@ -49,22 +41,9 @@
font-size: 4rem;
margin-top: 1.5rem;
}
}
.contact-details {
&__input {
width: 100%;
}
&__group {
margin: 20px 0;
display: flex;
align-items: baseline;
}
&__group-content {
display: flex;
flex-wrap: wrap;
max-width: 450px;
* {
min-width: 200px;
}
&__contacts-controls {
position: absolute;
right: 20px;
top: 5px;
}
}
@@ -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,164 +12,85 @@
// 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, ViewEncapsulation } 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);
},
});
}
}
ContactService,
contactTypeToViewReadyContact,
ViewReadyContact,
} from './contact.service';
@Component({
selector: 'app-contact',
templateUrl: './contact.component.html',
styleUrls: ['./contact.component.scss'],
encapsulation: ViewEncapsulation.None,
})
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.contactService.contacts()) {
this.dataSource = new MatTableDataSource<ViewReadyContact>(
this.contactService.contacts()
);
}
});
effect(() => {
if (this.registrarService.registrarId()) {
this.contactService.isContactDetailsView = false;
this.contactService.isContactNewView = false;
this.contactService.fetchContacts().pipe(take(1)).subscribe();
}
});
}
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: ViewReadyContact) {
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,103 @@
// 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?: ViewReadyContact) {
this.contactInEdit = contact
? contact
: contactTypeToViewReadyContact({
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) {
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,104 @@
// 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();
const request = this.contactService.isContactNewView
? this.contactService.addContact(this.contactService.contactInEdit)
: this.contactService.saveContacts(this.contactService.contacts());
request.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)
);
}
}
}
@@ -0,0 +1,76 @@
<div class="settings-security__edit-password">
<p>
<button
mat-icon-button
aria-label="Back to security settings"
(click)="goBack()"
>
<mat-icon>arrow_back</mat-icon>
</button>
</p>
<h1>Update EPP password</h1>
<p class="secondary-text">
Passwords must be between 6 and 16 alphanumeric characters
</p>
<form
(ngSubmit)="save()"
[formGroup]="passwordUpdateForm"
class="settings-security__edit-password-form"
>
<div class="settings-security__edit-password-field">
<mat-form-field appearance="outline">
<mat-label>Old password: </mat-label>
<input
matInput
type="text"
formControlName="oldPassword"
required
autocomplete="current-password"
/>
<mat-error *ngIf="hasError('oldPassword') as errorText">{{
errorText
}}</mat-error>
</mat-form-field>
</div>
<div class="settings-security__edit-password-field">
<mat-form-field appearance="outline">
<mat-label>New password: </mat-label>
<input
matInput
type="text"
formControlName="newPassword"
required
autocomplete="new-password"
/>
<mat-error *ngIf="hasError('newPassword') as errorText">{{
errorText
}}</mat-error>
</mat-form-field>
</div>
<div class="settings-security__edit-password-field">
<mat-form-field appearance="outline">
<mat-label>Confirm new password: </mat-label>
<input
matInput
type="text"
formControlName="newPasswordRepeat"
required
autocomplete="new-password"
/>
<mat-error *ngIf="hasError('newPasswordRepeat') as errorText">{{
errorText
}}</mat-error>
</mat-form-field>
</div>
<button
mat-flat-button
color="primary"
[disabled]="!passwordUpdateForm.valid"
aria-label="Save epp password update"
type="submit"
class="settings-security__edit-password-save"
>
Save
</button>
</form>
</div>
@@ -0,0 +1,16 @@
.settings-security__edit-password {
max-width: 616px;
&-field {
width: 100%;
mat-form-field {
margin-bottom: 20px;
width: 100%;
}
}
&-form {
margin-top: 30px;
}
&-save {
margin-top: 30px;
}
}
@@ -0,0 +1,122 @@
// 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 {
AbstractControl,
FormControl,
FormGroup,
ValidatorFn,
Validators,
} from '@angular/forms';
import { MatSnackBar } from '@angular/material/snack-bar';
import { RegistrarService } from 'src/app/registrar/registrar.service';
import { SecurityService } from './security.service';
type errorCode = 'required' | 'maxlength' | 'minlength' | 'passwordsDontMatch';
type errorFriendlyText = { [type in errorCode]: String };
@Component({
selector: 'app-epp-password-edit',
templateUrl: './eppPasswordEdit.component.html',
styleUrls: ['./eppPasswordEdit.component.scss'],
})
export default class EppPasswordEditComponent {
MIN_MAX_LENGHT = new String(
'Passwords must be between 6 and 16 alphanumeric characters'
);
errorTextMap: errorFriendlyText = {
required: "This field can't be empty",
maxlength: this.MIN_MAX_LENGHT,
minlength: this.MIN_MAX_LENGHT,
passwordsDontMatch: "Passwords don't match",
};
constructor(
public securityService: SecurityService,
private _snackBar: MatSnackBar,
public registrarService: RegistrarService
) {}
hasError(controlName: string) {
const maybeErrors = this.passwordUpdateForm.get(controlName)?.errors;
const maybeError =
maybeErrors && (Object.keys(maybeErrors)[0] as errorCode);
if (maybeError) {
return this.errorTextMap[maybeError];
}
return '';
}
newPasswordsMatch: ValidatorFn = (control: AbstractControl) => {
if (
this.passwordUpdateForm?.get('newPassword')?.value ===
this.passwordUpdateForm?.get('newPasswordRepeat')?.value
) {
this.passwordUpdateForm?.get('newPasswordRepeat')?.setErrors(null);
} else {
// latest angular just won't detect the error without setTimeout
setTimeout(() => {
this.passwordUpdateForm
?.get('newPasswordRepeat')
?.setErrors({ passwordsDontMatch: control.value });
});
}
return null;
};
passwordUpdateForm = new FormGroup({
oldPassword: new FormControl('', [Validators.required]),
newPassword: new FormControl('', [
Validators.required,
Validators.minLength(6),
Validators.maxLength(16),
this.newPasswordsMatch,
]),
newPasswordRepeat: new FormControl('', [
Validators.required,
Validators.minLength(6),
Validators.maxLength(16),
this.newPasswordsMatch,
]),
});
save() {
const { oldPassword, newPassword, newPasswordRepeat } =
this.passwordUpdateForm.value;
if (!oldPassword || !newPassword || !newPasswordRepeat) return;
this.securityService
.saveEppPassword({
registrarId: this.registrarService.registrarId(),
oldPassword,
newPassword,
newPasswordRepeat,
})
.subscribe({
complete: () => {
this.goBack();
},
error: (err: HttpErrorResponse) => {
this._snackBar.open(err.error);
},
});
}
goBack() {
this.securityService.isEditingPassword = false;
}
}
@@ -1,98 +1,123 @@
<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 if(securityService.isEditingPassword) {
<app-epp-password-edit></app-epp-password-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>EPP Password</h2>
<button
*ngIf="inEdit"
matSuffix
mat-icon-button
aria-label="Remove"
(click)="removeIpEntry(index)"
mat-flat-button
color="primary"
aria-label="Edit EPP Password"
(click)="editEppPassword()"
>
<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"
>Change the password used for EPP logins</span
>
</mat-list-item>
<mat-list-item role="listitem">
<span class="console-app__list-key">Password</span>
<span class="console-app__list-value">••••••••••••••</span>
</mat-list-item>
@if(dataSource.eppPasswordLastUpdated) {
<mat-divider></mat-divider>
<mat-list-item role="listitem">
<span class="console-app__list-key">Last Changed</span>
<span class="console-app__list-value">{{
dataSource.eppPasswordLastUpdated
}}</span>
</mat-list-item>
}
</mat-list>
</mat-card-content>
</mat-card>
<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
mat-flat-button
color="primary"
aria-label="Edit security settings"
(click)="editSecurity()"
>
<mat-icon>edit</mat-icon>
Edit
</button>
</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,38 @@
// 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 {
SecurityService,
RegistrarService,
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';
} from 'src/app/registrar/registrar.service';
import { SecurityService, apiToUiConverter } from './security.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);
editSecurity() {
this.securityService.isEditingSecurity = true;
}
resetDataSource() {
this.dataSource = apiToUiConverter(this.registrarService.registrar());
editEppPassword() {
this.securityService.isEditingPassword = 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.
@@ -14,22 +14,19 @@
import { Injectable } from '@angular/core';
import { switchMap } from 'rxjs';
import { RegistrarService } from 'src/app/registrar/registrar.service';
import {
IpAllowListItem,
RegistrarService,
SecuritySettings,
SecuritySettingsBackendModel,
} from 'src/app/registrar/registrar.service';
import { BackendService } from 'src/app/shared/services/backend.service';
interface ipAllowListItem {
value: string;
}
export interface SecuritySettings {
clientCertificate?: string;
failoverClientCertificate?: string;
ipAddressAllowList?: Array<ipAllowListItem>;
}
export interface SecuritySettingsBackendModel {
clientCertificate?: string;
failoverClientCertificate?: string;
ipAddressAllowList?: Array<string>;
export interface EppPasswordBackendModel {
registrarId: string;
oldPassword: string;
newPassword: string;
newPasswordRepeat: string;
}
export function apiToUiConverter(
@@ -48,13 +45,17 @@ export function uiToApiConverter(
return Object.assign({}, securitySettings, {
ipAddressAllowList: (securitySettings.ipAddressAllowList || [])
.filter((s) => s.value)
.map((ipAllowItem: ipAllowListItem) => ipAllowItem.value),
.map((ipAllowItem: IpAllowListItem) => ipAllowItem.value),
});
}
@Injectable()
@Injectable({
providedIn: 'root',
})
export class SecurityService {
securitySettings: SecuritySettings = {};
isEditingSecurity: boolean = false;
isEditingPassword: boolean = false;
constructor(
private backend: BackendService,
@@ -73,4 +74,12 @@ export class SecurityService {
})
);
}
saveEppPassword(data: EppPasswordBackendModel) {
return this.backend.postEppPasswordUpdate(data).pipe(
switchMap(() => {
return this.registrarService.loadRegistrars();
})
);
}
}
@@ -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>
@@ -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,12 +12,22 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component } from '@angular/core';
@Component({
selector: '[app-promotions-widget]',
templateUrl: './promotionsWidget.component.html',
})
export class PromotionsWidgetComponent {
constructor() {}
.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,64 @@
// 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 {
IpAllowListItem,
RegistrarService,
SecuritySettings,
} from 'src/app/registrar/registrar.service';
import { SecurityService, apiToUiConverter } 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,32 @@ 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?.countryCode) {
result += `${registrar?.localizedAddress?.countryCode} `;
}
if (registrar?.localizedAddress?.zip) {
result += registrar?.localizedAddress?.zip;
}
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,138 @@
<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 (line 1): </mat-label>
<input
matInput
type="text"
[ngModelOptions]="{ standalone: true }"
[(ngModel)]="registrarInEdit.localizedAddress.street![0]"
/>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Street Address (line 2): </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>

Some files were not shown because too many files have changed in this diff Show More