mirror of
https://github.com/google/nomulus
synced 2026-01-16 10:43:06 +00:00
Compare commits
287 Commits
nomulus-20
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c187c92ae4 | ||
|
|
22ca4e3f2b | ||
|
|
f27136458a | ||
|
|
d8e647316e | ||
|
|
d6e0a7b979 | ||
|
|
5725eb95e0 | ||
|
|
aa12998276 | ||
|
|
d415416bc5 | ||
|
|
3a1068f313 | ||
|
|
69e5d40f04 | ||
|
|
64f6cd9af4 | ||
|
|
40184689ca | ||
|
|
826ad85d20 | ||
|
|
2b47bc9b0a | ||
|
|
9555dca8c6 | ||
|
|
49484c06d3 | ||
|
|
81d222e7d6 | ||
|
|
7e9d4c27d1 | ||
|
|
f9c22ff1c5 | ||
|
|
2562d582f3 | ||
|
|
6f0bc1ded9 | ||
|
|
db9fc3271d | ||
|
|
84491fde70 | ||
|
|
0519e2ffcf | ||
|
|
85f75494ab | ||
|
|
cbba91558a | ||
|
|
c24f09febc | ||
|
|
fd51035f23 | ||
|
|
90eb078e3f | ||
|
|
2a94bdc257 | ||
|
|
50fa49e0c0 | ||
|
|
a581259edb | ||
|
|
fcdac3e86e | ||
|
|
b652f81193 | ||
|
|
d98d65eee5 | ||
|
|
28e72bd0d0 | ||
|
|
0777be3d6c | ||
|
|
f9cd167ae4 | ||
|
|
eed1886121 | ||
|
|
7149fd3307 | ||
|
|
0dc7ab99d7 | ||
|
|
76d4dfbb04 | ||
|
|
8547ad7941 | ||
|
|
b1266c95e8 | ||
|
|
bc9aab6790 | ||
|
|
6cb669a5a7 | ||
|
|
0f92e98028 | ||
|
|
5f0526c07a | ||
|
|
759aaddb5f | ||
|
|
816180f3b3 | ||
|
|
bf66b374c6 | ||
|
|
666cee1d9f | ||
|
|
d4a70c29a8 | ||
|
|
7b8d07954b | ||
|
|
34bea69a48 | ||
|
|
363800bd86 | ||
|
|
dee132d04b | ||
|
|
847ef12a4f | ||
|
|
d9349be18e | ||
|
|
0c74883428 | ||
|
|
b357fc79f7 | ||
|
|
754e7fbddc | ||
|
|
ad07b32638 | ||
|
|
8f69b48e87 | ||
|
|
c33f0dc07f | ||
|
|
969353d4e2 | ||
|
|
6cd351ec7c | ||
|
|
19e03dbd2e | ||
|
|
fc1eb162f2 | ||
|
|
ed25854fbc | ||
|
|
0aa6bc6aaa | ||
|
|
ff4c326ebe | ||
|
|
51b579871a | ||
|
|
b144aafb22 | ||
|
|
ddd955e156 | ||
|
|
6863f678f1 | ||
|
|
6bd90e967b | ||
|
|
5faf3d283c | ||
|
|
149fb66ac5 | ||
|
|
8c96940a27 | ||
|
|
9c5510f05d | ||
|
|
84884de77b | ||
|
|
d6c35df9bc | ||
|
|
7caa0ec9d6 | ||
|
|
ee3866ec4a | ||
|
|
97d0b7680f | ||
|
|
5700a008d6 | ||
|
|
dc9f5b99bc | ||
|
|
d3c6de7a38 | ||
|
|
3c3303c16a | ||
|
|
2a86a1bbe9 | ||
|
|
ea148ac13e | ||
|
|
06299ccb86 | ||
|
|
732c30b359 | ||
|
|
ee5a2d3916 | ||
|
|
2b5643df4c | ||
|
|
6bbd7a2290 | ||
|
|
77ab80f3dc | ||
|
|
5e1cd0120f | ||
|
|
0167dad85f | ||
|
|
1eaf3d4aa8 | ||
|
|
d9c46170dd | ||
|
|
e8a475f48b | ||
|
|
bdaab9daa5 | ||
|
|
7e07fabf7e | ||
|
|
16859bb36a | ||
|
|
7c92928f2c | ||
|
|
de8d205657 | ||
|
|
4738b979e4 | ||
|
|
a61a667992 | ||
|
|
1164070576 | ||
|
|
d23640a54f | ||
|
|
cc347264f1 | ||
|
|
e5d4cbb9fc | ||
|
|
8c1e0ff4de | ||
|
|
5cef2dd8b5 | ||
|
|
62b2585220 | ||
|
|
8692fe35db | ||
|
|
18614ba11e | ||
|
|
427f6db820 | ||
|
|
5aa40b2208 | ||
|
|
95c89bc856 | ||
|
|
c21b66f0fb | ||
|
|
b070c46231 | ||
|
|
a20eb8d1e9 | ||
|
|
338b8edb97 | ||
|
|
9f191e9392 | ||
|
|
39c2a79898 | ||
|
|
e2e9d4cfc7 | ||
|
|
2948dcc1be | ||
|
|
c5644d5c8b | ||
|
|
514d24ed67 | ||
|
|
c6868b771b | ||
|
|
f34aec8b56 | ||
|
|
b27b077638 | ||
|
|
0e8cd75a58 | ||
|
|
2a1748ba9c | ||
|
|
f4889191a4 | ||
|
|
9eddecf70f | ||
|
|
d4bcff0c31 | ||
|
|
62065f88fb | ||
|
|
c9ac9437fd | ||
|
|
1f6a09182d | ||
|
|
a0eff00031 | ||
|
|
89698c6ed6 | ||
|
|
a7696c3fac | ||
|
|
7ec599f849 | ||
|
|
70291af9ad | ||
|
|
5fb95f38ed | ||
|
|
dfe8e24761 | ||
|
|
bd30fcc81c | ||
|
|
8cecc8d3a8 | ||
|
|
c5a39bccc5 | ||
|
|
a90a117341 | ||
|
|
b40ad54daf | ||
|
|
b4d239c329 | ||
|
|
daa7ab3bfa | ||
|
|
56cd2ad282 | ||
|
|
0472dda860 | ||
|
|
083a9dc8c9 | ||
|
|
0153c6284a | ||
|
|
ca240adfb6 | ||
|
|
b17125ae9a | ||
|
|
dfef733360 | ||
|
|
04a0659197 | ||
|
|
70010886b1 | ||
|
|
3cd50dc929 | ||
|
|
03872b508f | ||
|
|
1096f201cd | ||
|
|
9dc3215624 | ||
|
|
af321fb65e | ||
|
|
c5132c04be | ||
|
|
a64dc21f96 | ||
|
|
0381533a35 | ||
|
|
4999a72d96 | ||
|
|
2d072c3844 | ||
|
|
c15dec4419 | ||
|
|
8340125bf4 | ||
|
|
98ba80d94e | ||
|
|
967d04efce | ||
|
|
20fd944e83 | ||
|
|
daa56e6d85 | ||
|
|
ed33c7424d | ||
|
|
04b30f5c04 | ||
|
|
11702bc940 | ||
|
|
2d82646421 | ||
|
|
50260dca5f | ||
|
|
3cc10bfe0d | ||
|
|
5645b2e218 | ||
|
|
2a01c12b14 | ||
|
|
93d77e558f | ||
|
|
92ebd0dedb | ||
|
|
b49e37feee | ||
|
|
bede56598c | ||
|
|
467d9c7bf1 | ||
|
|
e5ebe96c74 | ||
|
|
2ff4d97b0a | ||
|
|
6b0beeb477 | ||
|
|
d2d43f4115 | ||
|
|
12fd206c35 | ||
|
|
a3f510d0db | ||
|
|
fa54c26ee2 | ||
|
|
8896fb94f4 | ||
|
|
6c7bf5e5dd | ||
|
|
ea1e8d5cc5 | ||
|
|
7fb846c5b0 | ||
|
|
5180095cb6 | ||
|
|
9fe64bf9ec | ||
|
|
0f3b62d5ce | ||
|
|
bd4701647b | ||
|
|
fb816d7a2c | ||
|
|
8fbf363195 | ||
|
|
397f800614 | ||
|
|
bcf42bd287 | ||
|
|
ed95d19b93 | ||
|
|
97fc2c0b66 | ||
|
|
00728c40ba | ||
|
|
3f2a42ab8d | ||
|
|
b73e342820 | ||
|
|
df7fec7a3e | ||
|
|
6f7ae1eabc | ||
|
|
eb978ebbd5 | ||
|
|
95831bc8b7 | ||
|
|
538260521b | ||
|
|
612708f0a8 | ||
|
|
e78de98060 | ||
|
|
c918258fb1 | ||
|
|
34103ec815 | ||
|
|
a63812160e | ||
|
|
9aaf7ee36a | ||
|
|
96a864dbd6 | ||
|
|
8a36fb5f1f | ||
|
|
6c138420b0 | ||
|
|
08570511f5 | ||
|
|
e62d970d34 | ||
|
|
067927b735 | ||
|
|
4ec2919ce3 | ||
|
|
19422075fa | ||
|
|
40b6984ffb | ||
|
|
6952e0f653 | ||
|
|
dcb55d27bb | ||
|
|
765bd9834a | ||
|
|
221088e738 | ||
|
|
6649e00df7 | ||
|
|
2ceb52a7c4 | ||
|
|
120bcc33be | ||
|
|
8987fd37c2 | ||
|
|
653e092ad4 | ||
|
|
5e97a8b412 | ||
|
|
229fcf3946 | ||
|
|
b775e4a178 | ||
|
|
e3c386a8a7 | ||
|
|
799f0449ad | ||
|
|
bf025445d5 | ||
|
|
9f22f2e8ae | ||
|
|
45c8b81823 | ||
|
|
4cfcc60655 | ||
|
|
e4ee63b8f3 | ||
|
|
f8407c74bc | ||
|
|
693467a165 | ||
|
|
cea3da01a0 | ||
|
|
c2030e5859 | ||
|
|
1cbbc660d2 | ||
|
|
e0bbff827e | ||
|
|
10925f2447 | ||
|
|
7641b05f12 | ||
|
|
d130e74004 | ||
|
|
c9c61e4f17 | ||
|
|
da8df1f4d9 | ||
|
|
f649d960c1 | ||
|
|
e5ebc5a2bb | ||
|
|
f9d2839590 | ||
|
|
c6a6bc7e25 | ||
|
|
fce126d426 | ||
|
|
8e41278717 | ||
|
|
cb3738d540 | ||
|
|
71afc25110 | ||
|
|
fa377733be | ||
|
|
21950f7d82 | ||
|
|
e66aee0416 | ||
|
|
c7e1fc17d2 | ||
|
|
0c0b0df36e | ||
|
|
304f0002b4 | ||
|
|
15cf3e1bc0 | ||
|
|
eeed166310 | ||
|
|
e54075fea3 | ||
|
|
78cc1b2937 |
11
.gitignore
vendored
11
.gitignore
vendored
@@ -18,6 +18,13 @@ gjf.out
|
||||
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
||||
hs_err_pid*
|
||||
|
||||
# Environment-specific configuration files
|
||||
core/src/main/java/google/registry/config/files/nomulus-config-alpha.yaml
|
||||
core/src/main/java/google/registry/config/files/nomulus-config-crash.yaml
|
||||
core/src/main/java/google/registry/config/files/nomulus-config-production.yaml
|
||||
core/src/main/java/google/registry/config/files/nomulus-config-qa.yaml
|
||||
core/src/main/java/google/registry/config/files/nomulus-config-sandbox.yaml
|
||||
|
||||
######################################################################
|
||||
# Eclipse Ignores
|
||||
|
||||
@@ -114,9 +121,5 @@ core/**/registrar_dbg*.js
|
||||
core/**/registrar_bin*.css
|
||||
core/**/registrar_dbg*.css
|
||||
|
||||
# Appengine generated files
|
||||
core/WEB-INF/appengine-generated/*.bin
|
||||
core/WEB-INF/appengine-generated/*.xml
|
||||
|
||||
# jEnv
|
||||
.java-version
|
||||
|
||||
82
README.md
82
README.md
@@ -12,16 +12,16 @@ Nomulus is an open source, scalable, cloud-based service for operating
|
||||
[top-level domains](https://en.wikipedia.org/wiki/Top-level_domain) (TLDs). It
|
||||
is the authoritative source for the TLDs that it runs, meaning that it is
|
||||
responsible for tracking domain name ownership and handling registrations,
|
||||
renewals, availability checks, and WHOIS requests. End-user registrants (i.e.
|
||||
renewals, availability checks, and WHOIS requests. End-user registrants (i.e.,
|
||||
people or companies that want to register a domain name) use an intermediate
|
||||
domain name registrar acting on their behalf to interact with the registry.
|
||||
|
||||
Nomulus runs on [Google App Engine][gae] and is written primarily in Java. It is
|
||||
the software that [Google Registry](https://www.registry.google/) uses to
|
||||
operate TLDs such as .google, .app, .how, .soy, and .みんな. It can run any
|
||||
number of TLDs in a single shared registry system using horizontal scaling. Its
|
||||
source code is publicly available in this repository under the [Apache 2.0 free
|
||||
and open source license](https://www.apache.org/licenses/LICENSE-2.0).
|
||||
Nomulus runs on [Google Kubernetes Engine](https://cloud.google.com/kubernetes-engine)
|
||||
and is written primarily in Java. It is the software that
|
||||
[Google Registry](https://www.registry.google/) uses to operate TLDs such as .google,
|
||||
.app, .how, .soy, and .みんな. It can run any number of TLDs in a single shared registry
|
||||
system using horizontal scaling. Its source code is publicly available in this
|
||||
repository under the [Apache 2.0 free and open source license](https://www.apache.org/licenses/LICENSE-2.0).
|
||||
|
||||
## Getting started
|
||||
|
||||
@@ -30,8 +30,8 @@ running system:
|
||||
|
||||
* [Install
|
||||
guide](https://github.com/google/nomulus/blob/master/docs/install.md)
|
||||
* View the source code for the [GAE app](https://github.com/google/nomulus/tree/master/core/src/main/java/google/registry)
|
||||
and for the [GKE proxy](https://github.com/google/nomulus/tree/master/proxy/src/main/java/google/registry)
|
||||
* View the source code for the [Main HTTP server](https://github.com/google/nomulus/tree/master/core/src/main/java/google/registry)
|
||||
and for the [EPP proxy](https://github.com/google/nomulus/tree/master/proxy/src/main/java/google/registry)
|
||||
* [Other docs](https://github.com/google/nomulus/tree/master/docs)
|
||||
* [Javadoc](https://javadoc.nomulus.foo/)
|
||||
* [Nomulus discussion
|
||||
@@ -54,9 +54,11 @@ Nomulus has the following capabilities:
|
||||
checking, updating, and transferring domain names.
|
||||
* **[DNS](https://en.wikipedia.org/wiki/Domain_Name_System) interface**: The
|
||||
registry provides a pluggable interface that can be implemented to handle
|
||||
different DNS providers. It includes a sample implementation using Google
|
||||
Cloud DNS as well as an RFC 2136 compliant implementation that works with
|
||||
BIND.
|
||||
different DNS providers. It includes a sample implementation using [Google
|
||||
Cloud DNS](https://cloud.google.com/dns/), as well as an RFC 2136 compliant
|
||||
implementation that works with BIND. If you are using Google Cloud DNS, you
|
||||
may need to understand its capabilities and provide your own
|
||||
multi-[AS](https://en.wikipedia.org/wiki/Autonomous_system_\(Internet\)) solution.
|
||||
* **[WHOIS](https://en.wikipedia.org/wiki/WHOIS)**: A text-based protocol that
|
||||
returns ownership and contact information on registered domain names.
|
||||
* **[Registration Data Access Protocol
|
||||
@@ -68,7 +70,7 @@ Nomulus has the following capabilities:
|
||||
provider to allow take-over by another registry operator in the event of
|
||||
serious failure. This is required by ICANN for all [new
|
||||
gTLDs](https://newgtlds.icann.org/).
|
||||
* **Premium pricing**: Communicates prices for premium domain names (i.e.
|
||||
* **Premium pricing**: Communicates prices for premium domain names (i.e.,
|
||||
those that are highly desirable) and supports configurable premium
|
||||
registration and renewal prices. An extensible interface allows fully
|
||||
programmatic pricing.
|
||||
@@ -91,56 +93,50 @@ Nomulus has the following capabilities:
|
||||
* **Administrative tool**: Performs the full range of administrative tasks
|
||||
needed to manage a running registry system, including creating and
|
||||
configuring new TLDs.
|
||||
* **DNS interface**: An interface for DNS operations is provided so you can
|
||||
write an implementation for your chosen provider, along with a sample
|
||||
implementation that uses [Google Cloud DNS](https://cloud.google.com/dns/).
|
||||
If you are using Google Cloud DNS you may need to understand its
|
||||
capabilities and provide your own
|
||||
multi-[AS](https://en.wikipedia.org/wiki/Autonomous_system_\(Internet\))
|
||||
solution.
|
||||
* **GAE Proxy**: App Engine Standard only serves HTTP/S traffic. A proxy to
|
||||
forward traffic on EPP and WHOIS ports to App Engine via HTTPS is provided.
|
||||
Instructions on setting up the proxy on
|
||||
[Google Kubernetes Engine](https://cloud.google.com/kubernetes-engine/)
|
||||
is [available](https://github.com/google/nomulus/blob/master/docs/proxy-setup.md).
|
||||
Running the proxy on GKE supports IPv4 and IPv6 access, per ICANN's
|
||||
requirements for gTLDs. The proxy can also run as a single jar file, or on
|
||||
other Kubernetes providers, with modifications.
|
||||
* **Secure storage of cryptographic keys**: A keyring interface is
|
||||
provided for plugging in your own implementation (see [configuration
|
||||
doc](https://github.com/google/nomulus/blob/master/docs/configuration.md)
|
||||
for details), and an implementation based on
|
||||
[Google Cloud Secret Manager](https://cloud.google.com/security/products/secret-manager) is
|
||||
available.
|
||||
* **TPC Proxy**: Nomulus is built on top of the [Jetty](https://jetty.org/)
|
||||
container that implements the [Jakarta Servlet](https://jakarta.ee/specifications/servlet/)
|
||||
specification and only serves HTTP/S traffic. A proxy to translate raw TCP traffic (e.g., EPP)
|
||||
to and from HTTP is provided.
|
||||
Instructions on setting up the proxy
|
||||
are [available](https://github.com/google/nomulus/blob/master/docs/proxy-setup.md).
|
||||
The proxy can either run in a separate cluster and communicate to Nomulus public HTTP
|
||||
endpoints via the Internet, or as a sidecar with the Nomulus image in the same pod and
|
||||
communicate to it via loopback.
|
||||
|
||||
## Additional components
|
||||
|
||||
Registry operators interested in deploying Nomulus will likely require some
|
||||
additional components that are need to be configured separately.
|
||||
additional components that need to be configured separately.
|
||||
|
||||
* A way to invoice registrars for domain name registrations and accept
|
||||
payments. Nomulus records the information required to generate invoices in
|
||||
[billing
|
||||
events](https://github.com/google/nomulus/blob/master/docs/code-structure.md#billing-events).
|
||||
* Fully automated reporting to meet ICANN's requirements for gTLDs. Nomulus
|
||||
includes substantial reporting functionality but some additional work will
|
||||
includes substantial reporting functionality, but some additional work will
|
||||
be required by the operator in this area.
|
||||
* A secure method for storing cryptographic keys. A keyring interface is
|
||||
provided for plugging in your own implementation (see [configuration
|
||||
doc](https://github.com/google/nomulus/blob/master/docs/configuration.md)
|
||||
for details).
|
||||
|
||||
* System status and uptime monitoring.
|
||||
|
||||
## Outside references
|
||||
|
||||
* [Donuts](http://donuts.domains) Registry has helped review the code and
|
||||
provided valuable feedback
|
||||
* [Identity Digital](http://identity.digital) has helped review the code and
|
||||
provided valuable feedback.
|
||||
* [CoCCa](http://cocca.org.nz) and [FRED](https://fred.nic.cz) are other
|
||||
open-source registry platforms in use by many TLDs
|
||||
open-source registry platforms in use by many TLDs.
|
||||
* We are not aware of any fully open source domain registrar projects, but
|
||||
open source EPP Toolkits (not yet tested with Nomulus; may require
|
||||
integration work) include:
|
||||
* [EPP RTK Project](http://epp-rtk.sourceforge.net/)
|
||||
* [CentralNic](https://www.centralnic.com/registry/labs)
|
||||
* [Universal Registry/Registrar Toolkit](https://sourceforge.net/projects/epp-rtk/)
|
||||
* [ari-toolkit](https://github.com/AusRegistry/ari-toolkit)
|
||||
* [Net::DRI](https://metacpan.org/pod/Net::DRI)
|
||||
* Some Open Source DNS Projects that may be useful, but which we have not
|
||||
tested:
|
||||
* [AtomiaDNS](http://atomiadns.com/)
|
||||
* [PowerDNS](https://doc.powerdns.com/md/)
|
||||
|
||||
[gae]:https://cloud.google.com/appengine/docs/about-the-standard-environment
|
||||
* [AtomiaDNS](https://github.com/atomia/atomiadns)
|
||||
* [PowerDNS](https://github.com/PowerDNS/pdns)
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
// Copyright 2019 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.
|
||||
|
||||
apply plugin: 'war'
|
||||
|
||||
def environment = rootProject.environment
|
||||
def gcpProject = rootProject.gcpProject
|
||||
|
||||
// Set this directory before applying the appengine plugin so that the
|
||||
// plugin will recognize this as an app-engine standard app (and also
|
||||
// obtains the appengine-web.xml from the correct location)
|
||||
project.convention.plugins['war'].webAppDirName =
|
||||
"../../core/src/main/java/google/registry/env/${environment}/${project.name}"
|
||||
|
||||
apply plugin: 'com.google.cloud.tools.appengine'
|
||||
|
||||
def coreResourcesDir = "${rootDir}/core/build/resources/main"
|
||||
def coreLibsDir = "${rootDir}/core/build/libs"
|
||||
|
||||
// Get the web.xml file for the service.
|
||||
war {
|
||||
webInf {
|
||||
from "../../core/src/main/java/google/registry/env/common/${project.name}/WEB-INF"
|
||||
}
|
||||
}
|
||||
|
||||
war {
|
||||
from("${coreResourcesDir}/google/registry/ui/html") {
|
||||
include "*.html"
|
||||
}
|
||||
from("${coreLibsDir}") {
|
||||
include "core.jar"
|
||||
into("WEB-INF/lib")
|
||||
}
|
||||
}
|
||||
|
||||
if (project.path == ":services:default") {
|
||||
war {
|
||||
from("${coreResourcesDir}/google/registry/ui/html") {
|
||||
include "*.html"
|
||||
into("registrar")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
appengine {
|
||||
deploy {
|
||||
// appengineDeployAll task requires the version to be set. So,
|
||||
// this config lets gcloud select a version name when deploying
|
||||
// to alpha or sandbox from our workstation.
|
||||
if (!rootProject.prodOrSandboxEnv) {
|
||||
version = 'GCLOUD_CONFIG'
|
||||
}
|
||||
|
||||
// Don't set gcpProject directly, it gets overriden in ./build.gradle.
|
||||
// Do -P environment={crash,alpha} instead. For sandbox/production,
|
||||
// use Spinnaker.
|
||||
projectId = gcpProject
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(path: ':core', configuration: 'deploy_jar')
|
||||
}
|
||||
|
||||
// The tools.jar file gets pulled in from the java environment and for some
|
||||
// reason gets exploded "readonly", causing subsequent builds to fail when
|
||||
// they can't overwrite it. The hack below makes the file writable after
|
||||
// we're done exploding it.
|
||||
//
|
||||
// Fun fact: We only use this jar for documentation generation and as such we
|
||||
// don't need it in our warfile, as it is not used by the application at
|
||||
// runtime. But it's not clear how to exclude it, as we seem to be
|
||||
// constructing the jar from the entire WEB-INF directory and per-file
|
||||
// exclude rules don't seem to work on it. Better solutions are welcome :-)
|
||||
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 ':core:processResources'
|
||||
tasks['war'].dependsOn ':core:jar'
|
||||
|
||||
// Impose verification for all of the deployment tasks. We haven't found a
|
||||
// better way to do this other than to apply to each of them independently.
|
||||
// If a new task gets added, it will still fail if "environment" is not defined
|
||||
// because gcpProject is null. We just won't get as friendly an error message.
|
||||
appengineDeployAll.configure rootProject.verifyDeploymentConfig
|
||||
appengineDeploy.configure rootProject.verifyDeploymentConfig
|
||||
appengineDeployCron.configure rootProject.verifyDeploymentConfig
|
||||
appengineDeployDispatch.configure rootProject.verifyDeploymentConfig
|
||||
appengineDeployDos.configure rootProject.verifyDeploymentConfig
|
||||
appengineDeployIndex.configure rootProject.verifyDeploymentConfig
|
||||
appengineDeployQueue.configure rootProject.verifyDeploymentConfig
|
||||
32
build.gradle
32
build.gradle
@@ -84,10 +84,10 @@ tasks.build.dependsOn(tasks.checkLicense)
|
||||
// Paths to main and test sources.
|
||||
ext.projectRootDir = "${rootDir}"
|
||||
|
||||
// Tasks to deploy/stage all App Engine services
|
||||
// Tasks to deploy/stage all services
|
||||
task deploy {
|
||||
group = 'deployment'
|
||||
description = 'Deploys all services to App Engine.'
|
||||
description = 'Deploys all services.'
|
||||
}
|
||||
|
||||
task stage {
|
||||
@@ -95,26 +95,22 @@ task stage {
|
||||
description = 'Generates application directories for all services.'
|
||||
}
|
||||
|
||||
// App-engine environment configuration. We set up all of the variables in
|
||||
// the root project.
|
||||
|
||||
def environments = ['production', 'sandbox', 'alpha', 'crash', 'qa']
|
||||
|
||||
def gcpProject = null
|
||||
|
||||
apply from: "${rootDir.path}/projects.gradle"
|
||||
|
||||
if (environment == '') {
|
||||
// Keep the project null, this will prevent deployment. Set the
|
||||
// Keep the project null, this will prevent deployment. Set the
|
||||
// environment to "alpha" because other code needs this property to
|
||||
// explode the war file.
|
||||
environment = 'alpha'
|
||||
} else if (environment != 'production' && environment != 'sandbox') {
|
||||
} else {
|
||||
gcpProject = projects[environment]
|
||||
if (gcpProject == null) {
|
||||
throw new GradleException("-Penvironment must be one of " +
|
||||
"${projects.keySet()}.")
|
||||
}
|
||||
project(':console-webapp').setProperty('configuration', environment)
|
||||
}
|
||||
|
||||
rootProject.ext.environment = environment
|
||||
@@ -335,9 +331,6 @@ subprojects {
|
||||
|
||||
// Set up all of the deployment projects.
|
||||
if (services.contains(project.path)) {
|
||||
|
||||
apply from: "${rootDir.path}/appengine_war.gradle"
|
||||
|
||||
// Return early, do not apply the settings below.
|
||||
return
|
||||
}
|
||||
@@ -384,17 +377,6 @@ subprojects {
|
||||
}
|
||||
}
|
||||
|
||||
// Force SDK download and deployment to be sequential, otherwise parallel tasks
|
||||
// will fail. For SDK download, they will try to write to the same location to
|
||||
// upgrade gcloud. For deployment, they will try to deploy different services to
|
||||
// the same project at the same time.
|
||||
for (int i = 1; i < services.size(); i++) {
|
||||
project("${services[i]}").downloadCloudSdk
|
||||
.dependsOn(project("${services[i - 1]}").downloadCloudSdk)
|
||||
project("${services[i]}").appengineDeployAll
|
||||
.dependsOn(project("${services[i - 1]}").appengineDeployAll)
|
||||
}
|
||||
|
||||
// If "-P verboseTestOutput=true" is passed in, configure all subprojects to dump all of their
|
||||
// output and final test status (pass/fail, errors) for each test class.
|
||||
//
|
||||
@@ -561,7 +543,7 @@ task deployCloudSchedulerAndQueue {
|
||||
commandLine 'go', 'run',
|
||||
"./deployCloudSchedulerAndQueue.go",
|
||||
"${rootDir}/core/src/main/java/google/registry/config/files/nomulus-config-${env}.yaml",
|
||||
"${rootDir}/core/src/main/java/google/registry/env/${env}/default/WEB-INF/cloud-scheduler-tasks.xml",
|
||||
"${rootDir}/core/src/main/java/google/registry/config/files/tasks/cloud-scheduler-tasks-${env}.xml",
|
||||
"domain-registry-${env}"
|
||||
}
|
||||
exec {
|
||||
@@ -569,7 +551,7 @@ task deployCloudSchedulerAndQueue {
|
||||
commandLine 'go', 'run',
|
||||
"./deployCloudSchedulerAndQueue.go",
|
||||
"${rootDir}/core/src/main/java/google/registry/config/files/nomulus-config-${env}.yaml",
|
||||
"${rootDir}/core/src/main/java/google/registry/env/common/default/WEB-INF/cloud-tasks-queue.xml",
|
||||
"${rootDir}/core/src/main/java/google/registry/config/files/cloud-tasks-queue.xml",
|
||||
"domain-registry-${env}"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,4 +65,5 @@ dependencies {
|
||||
|
||||
testImplementation deps['org.junit.jupiter:junit-jupiter-api']
|
||||
testImplementation deps['org.junit.jupiter:junit-jupiter-engine']
|
||||
testImplementation deps['org.junit.platform:junit-platform-launcher']
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# This file is expected to be part of source control.
|
||||
aopalliance:aopalliance:1.0=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
|
||||
com.github.ben-manes.caffeine:caffeine:3.0.5=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
|
||||
com.github.ben-manes.caffeine:caffeine:3.1.8=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
|
||||
com.github.ben-manes.caffeine:caffeine:3.2.2=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
|
||||
com.github.kevinstern:software-and-algorithms:1.0=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
|
||||
com.google.auto.service:auto-service-annotations:1.0.1=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
|
||||
com.google.auto.value:auto-value-annotations:1.11.0=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
|
||||
@@ -12,13 +12,13 @@ com.google.auto:auto-common:1.2.1=annotationProcessor,errorprone,testAnnotationP
|
||||
com.google.code.findbugs:jsr305:3.0.2=annotationProcessor,checkstyle,compileClasspath,deploy_jar,errorprone,runtimeClasspath,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath,testing,testingAnnotationProcessor,testingCompileClasspath
|
||||
com.google.errorprone:error_prone_annotation:2.23.0=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
|
||||
com.google.errorprone:error_prone_annotations:2.23.0=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
|
||||
com.google.errorprone:error_prone_annotations:2.28.0=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
|
||||
com.google.errorprone:error_prone_annotations:2.40.0=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
|
||||
com.google.errorprone:error_prone_annotations:2.7.1=checkstyle
|
||||
com.google.errorprone:error_prone_check_api:2.23.0=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
|
||||
com.google.errorprone:error_prone_core:2.23.0=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
|
||||
com.google.errorprone:error_prone_type_annotations:2.23.0=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
|
||||
com.google.errorprone:javac:9+181-r4173-1=errorproneJavac
|
||||
com.google.flogger:flogger:0.8=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
|
||||
com.google.flogger:flogger:0.9=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
|
||||
com.google.guava:failureaccess:1.0.1=annotationProcessor,checkstyle,errorprone,testAnnotationProcessor,testingAnnotationProcessor
|
||||
com.google.guava:failureaccess:1.0.2=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
|
||||
com.google.guava:guava-parent:32.1.1-jre=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
|
||||
@@ -36,15 +36,15 @@ commons-beanutils:commons-beanutils:1.9.4=checkstyle
|
||||
commons-collections:commons-collections:3.2.2=checkstyle
|
||||
info.picocli:picocli:4.6.2=checkstyle
|
||||
io.github.eisop:dataflow-errorprone:3.34.0-eisop1=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
|
||||
io.github.java-diff-utils:java-diff-utils:4.12=annotationProcessor,compileClasspath,deploy_jar,errorprone,runtimeClasspath,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath,testing,testingAnnotationProcessor,testingCompileClasspath
|
||||
jakarta.inject:jakarta.inject-api:1.0.5=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
|
||||
io.github.java-diff-utils:java-diff-utils:4.12=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
|
||||
io.github.java-diff-utils:java-diff-utils:4.16=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
|
||||
jakarta.inject:jakarta.inject-api:2.0.1=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
|
||||
javax.inject:javax.inject:1=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
|
||||
joda-time:joda-time:2.13.0=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
|
||||
joda-time:joda-time:2.14.0=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
|
||||
junit:junit:4.13.2=testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
|
||||
net.sf.saxon:Saxon-HE:10.6=checkstyle
|
||||
org.antlr:antlr4-runtime:4.9.3=checkstyle
|
||||
org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath
|
||||
org.checkerframework:checker-compat-qual:2.5.3=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
|
||||
org.checkerframework:checker-qual:3.12.0=checkstyle
|
||||
org.checkerframework:checker-qual:3.33.0=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
|
||||
org.checkerframework:checker-qual:3.42.0=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
|
||||
@@ -54,12 +54,13 @@ org.jacoco:org.jacoco.ant:0.8.12=jacocoAnt
|
||||
org.jacoco:org.jacoco.core:0.8.12=jacocoAnt
|
||||
org.jacoco:org.jacoco.report:0.8.12=jacocoAnt
|
||||
org.javassist:javassist:3.28.0-GA=checkstyle
|
||||
org.jspecify:jspecify:0.3.0=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
|
||||
org.junit.jupiter:junit-jupiter-api:5.11.2=testCompileClasspath,testRuntimeClasspath
|
||||
org.junit.jupiter:junit-jupiter-engine:5.11.2=testCompileClasspath,testRuntimeClasspath
|
||||
org.junit.platform:junit-platform-commons:1.11.2=testCompileClasspath,testRuntimeClasspath
|
||||
org.junit.platform:junit-platform-engine:1.11.2=testCompileClasspath,testRuntimeClasspath
|
||||
org.junit:junit-bom:5.11.2=testCompileClasspath,testRuntimeClasspath
|
||||
org.jspecify:jspecify:1.0.0=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
|
||||
org.junit.jupiter:junit-jupiter-api:5.13.4=testCompileClasspath,testRuntimeClasspath
|
||||
org.junit.jupiter:junit-jupiter-engine:5.13.4=testCompileClasspath,testRuntimeClasspath
|
||||
org.junit.platform:junit-platform-commons:1.13.4=testCompileClasspath,testRuntimeClasspath
|
||||
org.junit.platform:junit-platform-engine:1.13.4=testCompileClasspath,testRuntimeClasspath
|
||||
org.junit.platform:junit-platform-launcher:1.13.4=testCompileClasspath,testRuntimeClasspath
|
||||
org.junit:junit-bom:5.13.4=testCompileClasspath,testRuntimeClasspath
|
||||
org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testRuntimeClasspath
|
||||
org.ow2.asm:asm-commons:9.7=jacocoAnt
|
||||
org.ow2.asm:asm-tree:9.7=jacocoAnt
|
||||
|
||||
@@ -33,8 +33,8 @@ public abstract class DateTimeUtils {
|
||||
/**
|
||||
* A date in the far future that we can treat as infinity.
|
||||
*
|
||||
* <p>This value is (2^63-1)/1000 rounded down. AppEngine stores dates as 64 bit microseconds, but
|
||||
* Java uses milliseconds, so this is the largest representable date that will survive a
|
||||
* <p>This value is (2^63-1)/1000 rounded down. Postgres can store dates as 64 bit microseconds,
|
||||
* but Java uses milliseconds, so this is the largest representable date that will survive a
|
||||
* round-trip through the database.
|
||||
*/
|
||||
public static final DateTime END_OF_TIME = new DateTime(Long.MAX_VALUE / 1000, DateTimeZone.UTC);
|
||||
|
||||
@@ -16,8 +16,8 @@ package google.registry.util;
|
||||
|
||||
import static org.joda.time.DateTimeZone.UTC;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
import javax.inject.Inject;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
/** Clock implementation that proxies to the real system clock. */
|
||||
|
||||
@@ -17,9 +17,9 @@ package google.registry.util;
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
|
||||
import com.google.common.util.concurrent.Uninterruptibles;
|
||||
import jakarta.inject.Inject;
|
||||
import java.io.Serializable;
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
import javax.inject.Inject;
|
||||
import org.joda.time.ReadableDuration;
|
||||
|
||||
/** Implementation of {@link Sleeper} for production use. */
|
||||
|
||||
@@ -63,6 +63,7 @@ public class TextDiffSubject extends Subject {
|
||||
|
||||
private final ImmutableList<String> actual;
|
||||
private DiffFormat diffFormat = DiffFormat.SIDE_BY_SIDE_MARKDOWN;
|
||||
private ImmutableList<String> comments = ImmutableList.of();
|
||||
|
||||
protected TextDiffSubject(FailureMetadata metadata, List<String> actual) {
|
||||
super(metadata, actual);
|
||||
@@ -83,13 +84,27 @@ public class TextDiffSubject extends Subject {
|
||||
return this;
|
||||
}
|
||||
|
||||
/** If set, ignore lines that start with the given string. */
|
||||
public TextDiffSubject ignoringLinesStartingWith(String... comments) {
|
||||
this.comments = ImmutableList.copyOf(comments);
|
||||
return this;
|
||||
}
|
||||
|
||||
private ImmutableList<String> filterComments(List<String> lines) {
|
||||
return lines.stream()
|
||||
.filter(line -> !line.isBlank())
|
||||
.filter(line -> comments.stream().noneMatch(line::startsWith))
|
||||
.collect(ImmutableList.toImmutableList());
|
||||
}
|
||||
|
||||
public void hasSameContentAs(List<String> expectedContent) {
|
||||
checkNotNull(expectedContent, "expectedContent");
|
||||
ImmutableList<String> expected = ImmutableList.copyOf(expectedContent);
|
||||
if (expected.equals(actual)) {
|
||||
ImmutableList<String> filteredExpected = filterComments(expectedContent);
|
||||
ImmutableList<String> filteredActual = filterComments(actual);
|
||||
if (filteredExpected.equals(filteredActual)) {
|
||||
return;
|
||||
}
|
||||
String diffString = diffFormat.generateDiff(expected, actual);
|
||||
String diffString = diffFormat.generateDiff(filteredExpected, filteredActual);
|
||||
failWithoutActual(
|
||||
Fact.simpleFact(
|
||||
Joiner.on('\n')
|
||||
|
||||
@@ -305,7 +305,6 @@
|
||||
{
|
||||
// "Apache License, Version 2.0".
|
||||
"moduleLicense": null,
|
||||
"moduleVersion": "26.26.0",
|
||||
"moduleName": "com.google.cloud:libraries-bom"
|
||||
},
|
||||
{
|
||||
@@ -370,7 +369,6 @@
|
||||
// "Apache License, Version 2.0".
|
||||
{
|
||||
"moduleLicense": null,
|
||||
"moduleVersion": "1.33.0",
|
||||
"moduleName": "io.opentelemetry:opentelemetry-bom"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -56,7 +56,7 @@ PROPERTIES_HEADER = """\
|
||||
# nom_build), run ./nom_build --help.
|
||||
#
|
||||
# DO NOT EDIT THIS FILE BY HAND
|
||||
org.gradle.jvmargs=-Xmx1024m
|
||||
org.gradle.jvmargs=-Xmx4096m
|
||||
org.gradle.caching=true
|
||||
org.gradle.parallel=true
|
||||
"""
|
||||
@@ -104,7 +104,7 @@ PROPERTIES = [
|
||||
Property('testFilter',
|
||||
'Comma separated list of test patterns, if specified run only '
|
||||
'these.'),
|
||||
Property('environment', 'GAE Environment for deployment and staging.'),
|
||||
Property('environment', 'Environment for deployment and staging.'),
|
||||
|
||||
# Cloud SQL properties
|
||||
Property('dbServer',
|
||||
@@ -117,28 +117,19 @@ PROPERTIES = [
|
||||
Property('dbUser', 'Database user name for use in connection'),
|
||||
Property('dbPassword', 'Database password for use in connection'),
|
||||
|
||||
Property('publish_repo',
|
||||
'Maven repository that hosts the Cloud SQL schema jar and the '
|
||||
'registry server test jars. Such jars are needed for '
|
||||
'server/schema integration tests. Please refer to <a '
|
||||
'href="./integration/README.md">integration project</a> for more '
|
||||
'information.'),
|
||||
Property('baseSchemaTag',
|
||||
'The nomulus version tag of the schema for use in the schema'
|
||||
'deployment integration test (:db:schemaIncrementalDeployTest)'),
|
||||
Property('schema_version',
|
||||
'The nomulus version tag of the schema for use in a database'
|
||||
'integration test.'),
|
||||
Property('nomulus_version',
|
||||
'The version of nomulus to test against in a database '
|
||||
'integration test.'),
|
||||
Property('dot_path',
|
||||
'The path to "dot", part of the graphviz package that converts '
|
||||
'a BEAM pipeline to image. Setting this property to empty string '
|
||||
'will disable image generation.',
|
||||
'/usr/bin/dot'),
|
||||
Property('pipeline',
|
||||
'The name of the Beam pipeline being staged.')
|
||||
'The name of the Beam pipeline being staged.'),
|
||||
Property('nomulus_env',
|
||||
'For use by scripts. Normally not set manually.'),
|
||||
Property('schema_env',
|
||||
'For use by scripts. Normally not set manually.'),
|
||||
Property('schemaTestArtifactsDir',
|
||||
'For use by scripts. Normally not set manually.')
|
||||
]
|
||||
|
||||
GRADLE_FLAGS = [
|
||||
|
||||
@@ -25,7 +25,11 @@ import textwrap
|
||||
import re
|
||||
|
||||
# We should never analyze any generated files
|
||||
UNIVERSALLY_SKIPPED_PATTERNS = {"/build/", "cloudbuild-caches", "/out/", ".git/", ".gradle/", "/dist/", "karma.conf.js", "polyfills.ts", "test.ts", "/docs/console-endpoints/"}
|
||||
UNIVERSALLY_SKIPPED_PATTERNS = {"/build/", "cloudbuild-caches", "/out/", ".git/",
|
||||
".gradle/", "/dist/", "/console-alpha/", "/console-crash/", "/console-qa",
|
||||
"/console-sandbox", "/console-production", "karma.conf.js", "polyfills.ts",
|
||||
"test.ts", "/docs/console-endpoints/", "/bin/generated-sources/",
|
||||
"/bin/generated-test-sources/", "src/main/generated", "src/test/generated"}
|
||||
# We can't rely on CI to have the Enum package installed so we do this instead.
|
||||
FORBIDDEN = 1
|
||||
REQUIRED = 2
|
||||
@@ -87,25 +91,22 @@ PRESUBMITS = {
|
||||
PresubmitCheck(
|
||||
r".*Copyright 20\d{2} The Nomulus Authors\. All Rights Reserved\.",
|
||||
("java", "js", "soy", "sql", "py", "sh", "gradle", "ts"), {
|
||||
".git", "/build/", "/bin/generated-sources/", "/bin/generated-test-sources/",
|
||||
"node_modules/", "LoggerConfig.java", "registrar_bin.",
|
||||
".git", "/build/", "node_modules/", "LoggerConfig.java", "registrar_bin.",
|
||||
"registrar_dbg.", "google-java-format-diff.py",
|
||||
"nomulus.golden.sql", "soyutils_usegoog.js", "javascript/checks.js",
|
||||
"/src/main/generated", "/src/test/generated"
|
||||
"nomulus.golden.sql", "soyutils_usegoog.js", "javascript/checks.js"
|
||||
}, REQUIRED):
|
||||
"File did not include the license header.",
|
||||
|
||||
# Files must end in a newline
|
||||
PresubmitCheck(r".*\n$", ("java", "js", "soy", "sql", "py", "sh", "gradle", "ts"),
|
||||
{"node_modules/"}, REQUIRED):
|
||||
PresubmitCheck(r".*\n$", ("java", "js", "soy", "sql", "py", "sh", "gradle", "ts", "xml"),
|
||||
{"node_modules/", ".idea"}, REQUIRED):
|
||||
"Source files must end in a newline.",
|
||||
|
||||
# System.(out|err).println should only appear in tools/ or load-testing/
|
||||
PresubmitCheck(
|
||||
r".*\bSystem\.(out|err)\.print", "java", {
|
||||
"StackdriverDashboardBuilder.java", "/tools/", "/example/",
|
||||
"/load-testing/", "RegistryTestServerMain.java",
|
||||
"TestServerExtension.java", "FlowDocumentationTool.java"
|
||||
"/tools/", "/example/", "/load-testing/",
|
||||
"RegistryTestServerMain.java", "TestServerExtension.java"
|
||||
}):
|
||||
"System.(out|err).println is only allowed in tools/ packages. Please "
|
||||
"use a logger instead.",
|
||||
@@ -118,7 +119,7 @@ PRESUBMITS = {
|
||||
):
|
||||
"In SOY please use the ({@param name: string} /** User name. */) style"
|
||||
" parameter passing instead of the ( * @param name User name.) style "
|
||||
"parameter pasing.",
|
||||
"parameter passing.",
|
||||
PresubmitCheck(
|
||||
r'.*\{[^}]+\w+:\s+"',
|
||||
"soy",
|
||||
@@ -137,41 +138,6 @@ PRESUBMITS = {
|
||||
{},
|
||||
):
|
||||
"All soy templates must use strict autoescaping",
|
||||
|
||||
# various JS linting checks
|
||||
PresubmitCheck(
|
||||
r".*goog\.base\(",
|
||||
"js",
|
||||
{"/node_modules/"},
|
||||
):
|
||||
"Use of goog.base is not allowed.",
|
||||
PresubmitCheck(
|
||||
r".*goog\.dom\.classes",
|
||||
"js",
|
||||
{"/node_modules/"},
|
||||
):
|
||||
"Instead of goog.dom.classes, use goog.dom.classlist which is smaller "
|
||||
"and faster.",
|
||||
PresubmitCheck(
|
||||
r".*goog\.getMsg",
|
||||
"js",
|
||||
{"/node_modules/"},
|
||||
):
|
||||
"Put messages in Soy, instead of using goog.getMsg().",
|
||||
PresubmitCheck(
|
||||
r".*(innerHTML|outerHTML)\s*(=|[+]=)([^=]|$)",
|
||||
"js",
|
||||
{"/node_modules/", "registrar_bin."},
|
||||
):
|
||||
"Do not assign directly to the dom. Use goog.dom.setTextContent to set"
|
||||
" to plain text, goog.dom.removeChildren to clear, or "
|
||||
"soy.renderElement to render anything else",
|
||||
PresubmitCheck(
|
||||
r".*console\.(log|info|warn|error)",
|
||||
"js",
|
||||
{"/node_modules/", "google/registry/ui/js/util.js", "registrar_bin."},
|
||||
):
|
||||
"JavaScript files should not include console logging.",
|
||||
PresubmitCheck(
|
||||
r".*\nimport (static )?.*\.shaded\..*",
|
||||
"java",
|
||||
@@ -208,6 +174,12 @@ PRESUBMITS = {
|
||||
{"/node_modules/"},
|
||||
):
|
||||
"Do not use javax.servlet.* Use jakarta.servlet.* instead.",
|
||||
PresubmitCheck(
|
||||
r".*javax\.inject\..*",
|
||||
"java",
|
||||
{"/node_modules/"},
|
||||
):
|
||||
"Do not use javax.inject.* Use jakarta.inject.* instead.",
|
||||
}
|
||||
|
||||
# Note that this regex only works for one kind of Flyway file. If we want to
|
||||
@@ -295,26 +267,6 @@ def verify_flyway_index():
|
||||
return not success
|
||||
|
||||
|
||||
def verify_javascript_deps():
|
||||
"""Verifies that we haven't introduced any new javascript dependencies."""
|
||||
with open('package.json') as f:
|
||||
package = json.load(f)
|
||||
|
||||
deps = list(package['dependencies'].keys())
|
||||
if deps != EXPECTED_JS_PACKAGES:
|
||||
print('Unexpected javascript dependencies. Was expecting '
|
||||
'%s, got %s.' % (EXPECTED_JS_PACKAGES, deps))
|
||||
print(textwrap.dedent("""
|
||||
* If the new dependencies are intentional, please verify that the
|
||||
* license is one of the allowed licenses (see
|
||||
* config/dependency-license/allowed_licenses.json) and add an entry
|
||||
* for the package (with the license in a comment) to the
|
||||
* EXPECTED_JS_PACKAGES variable in config/presubmits.py.
|
||||
"""))
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def get_files():
|
||||
for root, dirnames, filenames in os.walk("."):
|
||||
for filename in filenames:
|
||||
@@ -339,8 +291,5 @@ if __name__ == "__main__":
|
||||
# when we put it here it fails fast before all of the tests are run.
|
||||
failed |= verify_flyway_index()
|
||||
|
||||
# Make sure we haven't introduced any javascript dependencies.
|
||||
failed |= verify_javascript_deps()
|
||||
|
||||
if failed:
|
||||
sys.exit(1)
|
||||
|
||||
1
console-webapp/.gitignore
vendored
1
console-webapp/.gitignore
vendored
@@ -44,3 +44,4 @@ Thumbs.db
|
||||
|
||||
# Build artifact
|
||||
/staged/dist
|
||||
/staged/console-*
|
||||
|
||||
@@ -9,7 +9,7 @@ expected to change.
|
||||
|
||||
## Deployment
|
||||
|
||||
Webapp is deployed with the nomulus default service war to Google App Engine.
|
||||
The webapp is deployed with the nomulus default service war to GKE.
|
||||
During nomulus default service war build task, gradle script triggers the
|
||||
following:
|
||||
|
||||
|
||||
@@ -96,11 +96,17 @@
|
||||
"sourceMap": true,
|
||||
"namedChunks": true
|
||||
},
|
||||
"development": {
|
||||
"qa": {
|
||||
"optimization": false,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true,
|
||||
"namedChunks": true
|
||||
},
|
||||
"development": {
|
||||
"optimization": false,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true,
|
||||
"namedChunks": true,
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
@@ -120,6 +126,9 @@
|
||||
"sandbox": {
|
||||
"buildTarget": "console-webapp:build:sandbox"
|
||||
},
|
||||
"qa": {
|
||||
"buildTarget": "console-webapp:build:qa"
|
||||
},
|
||||
"development": {
|
||||
"buildTarget": "console-webapp:build:development"
|
||||
}
|
||||
|
||||
@@ -40,15 +40,55 @@ task runConsoleWebappUnitTests(type: Exec) {
|
||||
task buildConsoleWebapp(type: Exec) {
|
||||
workingDir "${consoleDir}/"
|
||||
executable 'npm'
|
||||
def configuration = project.hasProperty('configuration') ?
|
||||
project.getProperty('configuration') :
|
||||
'production'
|
||||
def configuration = project.getProperty('configuration')
|
||||
args 'run', "build", "--configuration=${configuration}"
|
||||
doFirst {
|
||||
println "Building console for environment: ${configuration}"
|
||||
}
|
||||
}
|
||||
|
||||
task buildConsoleForAll() {}
|
||||
|
||||
def createConsoleTask = { env ->
|
||||
project.tasks.register("buildConsoleFor${env.capitalize()}", Exec) {
|
||||
workingDir "${consoleDir}/"
|
||||
executable 'npm'
|
||||
args 'run', 'build', "--configuration=${env}"
|
||||
doFirst {
|
||||
println "Building console for environment: ${env}"
|
||||
}
|
||||
doLast {
|
||||
copy {
|
||||
from "${consoleDir}/staged/dist/"
|
||||
into "${consoleDir}/staged/console-${env}"
|
||||
}
|
||||
delete "${consoleDir}/staged/dist"
|
||||
}
|
||||
dependsOn(tasks.npmInstallDeps)
|
||||
}
|
||||
project.tasks.register("deleteConsoleFor${env.capitalize()}", Delete) {
|
||||
delete "${consoleDir}/staged/console-${env}"
|
||||
}
|
||||
tasks.named('clean') {
|
||||
dependsOn(tasks.named("deleteConsoleFor${env.capitalize()}"))
|
||||
}
|
||||
tasks.named('buildConsoleForAll') {
|
||||
dependsOn(tasks.named("buildConsoleFor${env.capitalize()}"))
|
||||
}
|
||||
}
|
||||
|
||||
['alpha', 'crash', 'qa', 'sandbox', 'production'].forEach {env ->
|
||||
createConsoleTask(env)
|
||||
}
|
||||
|
||||
// Force an order so we don't run these tasks in parallel.
|
||||
tasks.buildConsoleForCrash.mustRunAfter(tasks.buildConsoleForAlpha)
|
||||
tasks.buildConsoleForQa.mustRunAfter(tasks.buildConsoleForCrash)
|
||||
tasks.buildConsoleForSandbox.mustRunAfter(tasks.buildConsoleForQa)
|
||||
tasks.buildConsoleForProduction.mustRunAfter(tasks.buildConsoleForSandbox)
|
||||
// This task must run last, otherwise the previous tasks will have deleted the "dist" folder.
|
||||
tasks.buildConsoleWebapp.mustRunAfter(tasks.buildConsoleForProduction)
|
||||
|
||||
task applyFormatting(type: Exec) {
|
||||
workingDir "${consoleDir}/"
|
||||
executable 'npm'
|
||||
@@ -61,16 +101,9 @@ task checkFormatting(type: Exec) {
|
||||
args 'run', 'prettify:check'
|
||||
}
|
||||
|
||||
task deploy(type: Exec) {
|
||||
workingDir "${consoleDir}/staged"
|
||||
executable 'gcloud'
|
||||
args 'app', 'deploy', "${projectParam}", '--quiet'
|
||||
}
|
||||
|
||||
tasks.buildConsoleWebapp.dependsOn(tasks.npmInstallDeps)
|
||||
tasks.runConsoleWebappUnitTests.dependsOn(tasks.npmInstallDeps)
|
||||
tasks.applyFormatting.dependsOn(tasks.npmInstallDeps)
|
||||
tasks.checkFormatting.dependsOn(tasks.npmInstallDeps)
|
||||
tasks.build.dependsOn(tasks.checkFormatting)
|
||||
tasks.build.dependsOn(tasks.runConsoleWebappUnitTests)
|
||||
tasks.deploy.dependsOn(tasks.buildConsoleWebapp)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"/console-api":
|
||||
{
|
||||
"target": "http://localhost:8080",
|
||||
"target": "http://[::1]:8080",
|
||||
"secure": false,
|
||||
"logLevel": "debug",
|
||||
"changeOrigin": true
|
||||
|
||||
1
console-webapp/gradle.properties
Normal file
1
console-webapp/gradle.properties
Normal file
@@ -0,0 +1 @@
|
||||
configuration=production
|
||||
12263
console-webapp/package-lock.json
generated
12263
console-webapp/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -16,31 +16,31 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^18.0.2",
|
||||
"@angular/cdk": "^18.0.2",
|
||||
"@angular/common": "^18.0.2",
|
||||
"@angular/compiler": "^18.0.2",
|
||||
"@angular/core": "^18.0.2",
|
||||
"@angular/forms": "^18.0.2",
|
||||
"@angular/material": "^18.0.2",
|
||||
"@angular/platform-browser": "^18.0.2",
|
||||
"@angular/platform-browser-dynamic": "^18.0.2",
|
||||
"@angular/router": "^18.0.2",
|
||||
"@angular/animations": "^19.1.4",
|
||||
"@angular/cdk": "^19.1.2",
|
||||
"@angular/common": "^19.1.4",
|
||||
"@angular/compiler": "^19.1.4",
|
||||
"@angular/core": "^19.1.4",
|
||||
"@angular/forms": "^19.1.4",
|
||||
"@angular/material": "^19.1.2",
|
||||
"@angular/platform-browser": "^19.1.4",
|
||||
"@angular/platform-browser-dynamic": "^19.1.4",
|
||||
"@angular/router": "^19.1.4",
|
||||
"rxjs": "~7.5.0",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.14.2"
|
||||
"zone.js": "~0.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^18.0.3",
|
||||
"@angular-eslint/builder": "18.0.1",
|
||||
"@angular-eslint/eslint-plugin": "18.0.1",
|
||||
"@angular-eslint/eslint-plugin-template": "18.0.1",
|
||||
"@angular-eslint/schematics": "18.0.1",
|
||||
"@angular-eslint/template-parser": "18.0.1",
|
||||
"@angular/cli": "~18.0.3",
|
||||
"@angular/compiler-cli": "^18.0.2",
|
||||
"@angular-devkit/build-angular": "^19.1.5",
|
||||
"@angular-eslint/builder": "19.0.2",
|
||||
"@angular-eslint/eslint-plugin": "19.0.2",
|
||||
"@angular-eslint/eslint-plugin-template": "19.0.2",
|
||||
"@angular-eslint/schematics": "19.0.2",
|
||||
"@angular-eslint/template-parser": "19.0.2",
|
||||
"@angular/cli": "~19.1.5",
|
||||
"@angular/compiler-cli": "^19.1.4",
|
||||
"@types/jasmine": "~4.0.0",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/node": "^18.19.74",
|
||||
"@typescript-eslint/eslint-plugin": "^7.2.0",
|
||||
"@typescript-eslint/parser": "^7.2.0",
|
||||
"concurrently": "^7.6.0",
|
||||
@@ -52,6 +52,6 @@
|
||||
"karma-jasmine": "~5.1.0",
|
||||
"karma-jasmine-html-reporter": "~2.0.0",
|
||||
"prettier": "2.8.7",
|
||||
"typescript": "~5.4.5"
|
||||
"typescript": "^5.7.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,8 +24,10 @@ 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 WhoisComponent from './settings/whois/whois.component';
|
||||
import { SupportComponent } from './support/support.component';
|
||||
import RdapComponent from './settings/rdap/rdap.component';
|
||||
import { HistoryComponent } from './history/history.component';
|
||||
import { PasswordResetVerifyComponent } from './shared/components/passwordReset/passwordResetVerify.component';
|
||||
|
||||
export interface RouteWithIcon extends Route {
|
||||
iconName?: string;
|
||||
@@ -38,6 +40,10 @@ export const PATHS = {
|
||||
};
|
||||
export const routes: RouteWithIcon[] = [
|
||||
{ path: '', redirectTo: '/home', pathMatch: 'full' },
|
||||
{
|
||||
path: PasswordResetVerifyComponent.PATH,
|
||||
component: PasswordResetVerifyComponent,
|
||||
},
|
||||
{
|
||||
path: RegistryLockVerifyComponent.PATH,
|
||||
component: RegistryLockVerifyComponent,
|
||||
@@ -59,13 +65,18 @@ export const routes: RouteWithIcon[] = [
|
||||
title: 'Dashboard',
|
||||
iconName: 'view_comfy_alt',
|
||||
},
|
||||
// { path: 'tlds', component: TldsComponent, title: "TLDs", iconName: "event_list" },
|
||||
{
|
||||
path: DomainListComponent.PATH,
|
||||
component: DomainListComponent,
|
||||
title: 'Domains',
|
||||
iconName: 'view_list',
|
||||
},
|
||||
{
|
||||
path: HistoryComponent.PATH,
|
||||
component: HistoryComponent,
|
||||
// title: 'History',
|
||||
// iconName: 'history',
|
||||
},
|
||||
{
|
||||
path: SettingsComponent.PATH,
|
||||
component: SettingsComponent,
|
||||
@@ -83,9 +94,9 @@ export const routes: RouteWithIcon[] = [
|
||||
title: 'Contacts',
|
||||
},
|
||||
{
|
||||
path: WhoisComponent.PATH,
|
||||
component: WhoisComponent,
|
||||
title: 'WHOIS Info',
|
||||
path: RdapComponent.PATH,
|
||||
component: RdapComponent,
|
||||
title: 'RDAP Info',
|
||||
},
|
||||
{
|
||||
path: SecurityComponent.PATH,
|
||||
|
||||
@@ -7,18 +7,19 @@
|
||||
></mat-progress-bar>
|
||||
</div>
|
||||
<mat-sidenav-container class="console-app__container">
|
||||
<mat-sidenav-content class="console-app__content-wrapper">
|
||||
<div class="console-app__content" role="main">
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
</mat-sidenav-content>
|
||||
<mat-sidenav
|
||||
[mode]="breakpointObserver.isMobileView() ? 'over' : 'side'"
|
||||
[opened]="!breakpointObserver.isMobileView()"
|
||||
[disableClose]="!breakpointObserver.isMobileView()"
|
||||
#sidenav
|
||||
class="console-app__sidebar"
|
||||
>
|
||||
<app-navigation />
|
||||
</mat-sidenav>
|
||||
<mat-sidenav-content class="console-app__content-wrapper">
|
||||
<div class="console-app__content">
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
</mat-sidenav-content>
|
||||
</mat-sidenav-container>
|
||||
</div>
|
||||
|
||||
@@ -14,29 +14,124 @@
|
||||
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
import { provideHttpClientTesting } from '@angular/common/http/testing';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { ComponentFixture, fakeAsync, TestBed } from '@angular/core/testing';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { AppComponent } from './app.component';
|
||||
import { MaterialModule } from './material.module';
|
||||
import { BackendService } from './shared/services/backend.service';
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { routes } from './app-routing.module';
|
||||
import { AppModule } from './app.module';
|
||||
import { PocReminderComponent } from './shared/components/pocReminder/pocReminder.component';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { UserData, UserDataService } from './shared/services/userData.service';
|
||||
import { Registrar, RegistrarService } from './registrar/registrar.service';
|
||||
import { MatSidenavModule } from '@angular/material/sidenav';
|
||||
import { signal, WritableSignal } from '@angular/core';
|
||||
|
||||
describe('AppComponent', () => {
|
||||
let component: AppComponent;
|
||||
let fixture: ComponentFixture<AppComponent>;
|
||||
let mockRegistrarService: {
|
||||
registrar: WritableSignal<Partial<Registrar> | null | undefined>;
|
||||
registrarId: WritableSignal<string>;
|
||||
registrars: WritableSignal<Array<Partial<Registrar>>>;
|
||||
};
|
||||
let mockUserDataService: { userData: WritableSignal<Partial<UserData>> };
|
||||
let mockSnackBar: jasmine.SpyObj<MatSnackBar>;
|
||||
|
||||
const dummyPocReminderComponent = class {}; // Dummy class for type checking
|
||||
|
||||
beforeEach(async () => {
|
||||
mockRegistrarService = {
|
||||
registrar: signal<Registrar | null | undefined>(undefined),
|
||||
registrarId: signal('123'),
|
||||
registrars: signal([]),
|
||||
};
|
||||
|
||||
mockUserDataService = {
|
||||
userData: signal({
|
||||
globalRole: 'NONE',
|
||||
}),
|
||||
};
|
||||
|
||||
mockSnackBar = jasmine.createSpyObj('MatSnackBar', ['openFromComponent']);
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [AppComponent],
|
||||
imports: [MaterialModule, BrowserAnimationsModule, AppRoutingModule],
|
||||
imports: [
|
||||
MatSidenavModule,
|
||||
NoopAnimationsModule,
|
||||
MatSnackBarModule,
|
||||
AppModule,
|
||||
RouterModule.forRoot(routes),
|
||||
],
|
||||
providers: [
|
||||
BackendService,
|
||||
{ provide: RegistrarService, useValue: mockRegistrarService },
|
||||
{ provide: UserDataService, useValue: mockUserDataService },
|
||||
{ provide: MatSnackBar, useValue: mockSnackBar },
|
||||
{ provide: PocReminderComponent, useClass: dummyPocReminderComponent },
|
||||
provideHttpClient(),
|
||||
provideHttpClientTesting(),
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(AppComponent);
|
||||
component = fixture.componentInstance;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jasmine.clock().uninstall();
|
||||
});
|
||||
|
||||
it('should create the app', () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
fixture.detectChanges();
|
||||
const app = fixture.componentInstance;
|
||||
expect(app).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('PoC Verification Reminder', () => {
|
||||
beforeEach(() => {
|
||||
jasmine.clock().install();
|
||||
});
|
||||
|
||||
it('should open snackbar if lastPocVerificationDate is older than one year', fakeAsync(() => {
|
||||
const MOCK_TODAY = new Date('2024-07-15T10:00:00.000Z');
|
||||
jasmine.clock().mockDate(MOCK_TODAY);
|
||||
|
||||
const twoYearsAgo = new Date(MOCK_TODAY);
|
||||
twoYearsAgo.setFullYear(MOCK_TODAY.getFullYear() - 2);
|
||||
|
||||
mockRegistrarService.registrar.set({
|
||||
lastPocVerificationDate: twoYearsAgo.toISOString(),
|
||||
});
|
||||
|
||||
fixture.detectChanges();
|
||||
TestBed.flushEffects();
|
||||
|
||||
expect(mockSnackBar.openFromComponent).toHaveBeenCalledWith(
|
||||
PocReminderComponent,
|
||||
{
|
||||
horizontalPosition: 'center',
|
||||
verticalPosition: 'top',
|
||||
duration: 1000000000,
|
||||
}
|
||||
);
|
||||
}));
|
||||
|
||||
it('should NOT open snackbar if lastPocVerificationDate is within last year', fakeAsync(() => {
|
||||
const MOCK_TODAY = new Date('2024-07-15T10:00:00.000Z');
|
||||
jasmine.clock().mockDate(MOCK_TODAY);
|
||||
|
||||
const sixMonthsAgo = new Date(MOCK_TODAY);
|
||||
sixMonthsAgo.setMonth(MOCK_TODAY.getMonth() - 6);
|
||||
|
||||
mockRegistrarService.registrar.set({
|
||||
lastPocVerificationDate: sixMonthsAgo.toISOString(),
|
||||
});
|
||||
|
||||
fixture.detectChanges();
|
||||
TestBed.flushEffects();
|
||||
|
||||
expect(mockSnackBar.openFromComponent).not.toHaveBeenCalled();
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,18 +12,21 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { AfterViewInit, Component, ViewChild } from '@angular/core';
|
||||
import { AfterViewInit, Component, effect, 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';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { PocReminderComponent } from './shared/components/pocReminder/pocReminder.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.scss'],
|
||||
standalone: false,
|
||||
})
|
||||
export class AppComponent implements AfterViewInit {
|
||||
@ViewChild(MatSidenav)
|
||||
@@ -34,8 +37,28 @@ export class AppComponent implements AfterViewInit {
|
||||
protected userDataService: UserDataService,
|
||||
protected globalLoader: GlobalLoaderService,
|
||||
protected breakpointObserver: BreakPointObserverService,
|
||||
private router: Router
|
||||
) {}
|
||||
private router: Router,
|
||||
private _snackBar: MatSnackBar
|
||||
) {
|
||||
effect(() => {
|
||||
const registrar = this.registrarService.registrar();
|
||||
const oneYearAgo = new Date();
|
||||
oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);
|
||||
oneYearAgo.setHours(0, 0, 0, 0);
|
||||
if (
|
||||
registrar &&
|
||||
registrar.lastPocVerificationDate &&
|
||||
new Date(registrar.lastPocVerificationDate) < oneYearAgo &&
|
||||
this.userDataService?.userData()?.globalRole === 'NONE'
|
||||
) {
|
||||
this._snackBar.openFromComponent(PocReminderComponent, {
|
||||
horizontalPosition: 'center',
|
||||
verticalPosition: 'top',
|
||||
duration: 1000000000,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.router.events.subscribe((event) => {
|
||||
|
||||
@@ -26,7 +26,11 @@ import { BackendService } from './shared/services/backend.service';
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field';
|
||||
import { BillingInfoComponent } from './billingInfo/billingInfo.component';
|
||||
import { DomainListComponent } from './domains/domainList.component';
|
||||
import {
|
||||
DomainListComponent,
|
||||
ReasonDialogComponent,
|
||||
ResponseDialogComponent,
|
||||
} from './domains/domainList.component';
|
||||
import { RegistryLockComponent } from './domains/registryLock.component';
|
||||
import { HeaderComponent } from './header/header.component';
|
||||
import { HomeComponent } from './home/home.component';
|
||||
@@ -43,8 +47,6 @@ import EppPasswordEditComponent from './settings/security/eppPasswordEdit.compon
|
||||
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';
|
||||
@@ -54,7 +56,14 @@ 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';
|
||||
import { ForceFocusDirective } from './shared/directives/forceFocus.directive';
|
||||
import RdapComponent from './settings/rdap/rdap.component';
|
||||
import RdapEditComponent from './settings/rdap/rdapEdit.component';
|
||||
import { PocReminderComponent } from './shared/components/pocReminder/pocReminder.component';
|
||||
import { PasswordResetVerifyComponent } from './shared/components/passwordReset/passwordResetVerify.component';
|
||||
import { PasswordInputForm } from './shared/components/passwordReset/passwordInputForm.component';
|
||||
import { HistoryComponent } from './history/history.component';
|
||||
import { HistoryListComponent } from './history/historyList.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [SelectedRegistrarWrapper],
|
||||
@@ -71,27 +80,34 @@ export class SelectedRegistrarModule {}
|
||||
ContactDetailsComponent,
|
||||
DomainListComponent,
|
||||
EppPasswordEditComponent,
|
||||
ForceFocusDirective,
|
||||
HeaderComponent,
|
||||
HistoryComponent,
|
||||
HistoryListComponent,
|
||||
HomeComponent,
|
||||
LocationBackDirective,
|
||||
UserLevelVisibility,
|
||||
NavigationComponent,
|
||||
NewRegistrarComponent,
|
||||
NotificationsComponent,
|
||||
PasswordInputForm,
|
||||
PasswordResetVerifyComponent,
|
||||
PocReminderComponent,
|
||||
RdapComponent,
|
||||
RdapEditComponent,
|
||||
ReasonDialogComponent,
|
||||
RegistrarComponent,
|
||||
RegistrarDetailsComponent,
|
||||
RegistryLockComponent,
|
||||
RegistrarSelectorComponent,
|
||||
RegistryLockComponent,
|
||||
RegistryLockVerifyComponent,
|
||||
ResourcesComponent,
|
||||
ResponseDialogComponent,
|
||||
SecurityComponent,
|
||||
SecurityEditComponent,
|
||||
SettingsComponent,
|
||||
SettingsContactComponent,
|
||||
SupportComponent,
|
||||
TldsComponent,
|
||||
WhoisComponent,
|
||||
WhoisEditComponent,
|
||||
UserLevelVisibility,
|
||||
],
|
||||
bootstrap: [AppComponent],
|
||||
imports: [
|
||||
@@ -100,8 +116,8 @@ export class SelectedRegistrarModule {}
|
||||
BrowserModule,
|
||||
FormsModule,
|
||||
MaterialModule,
|
||||
SnackBarModule,
|
||||
SelectedRegistrarModule,
|
||||
SnackBarModule,
|
||||
],
|
||||
providers: [
|
||||
BackendService,
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
<app-selected-registrar-wrapper>
|
||||
<h1 class="mat-headline-4">Billing Info</h1>
|
||||
<h1 class="mat-headline-4" forceFocus>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"
|
||||
<a
|
||||
class="text-l"
|
||||
href="{{ driveFolderUrl() }}"
|
||||
target="_blank"
|
||||
aria-label="View billing records on Google Drive"
|
||||
>View on Google Drive</a
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<img src="./assets/billing.png" />
|
||||
<img src="./assets/billing.png" alt="Generic billing image" />
|
||||
</div>
|
||||
</div>
|
||||
</app-selected-registrar-wrapper>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
width: 100%;
|
||||
}
|
||||
&-subhead {
|
||||
font-size: 20px;
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
selector: 'app-billingInfo',
|
||||
templateUrl: './billingInfo.component.html',
|
||||
styleUrls: ['./billingInfo.component.scss'],
|
||||
standalone: false,
|
||||
})
|
||||
export class BillingInfoComponent {
|
||||
public static PATH = 'billingInfo';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<app-selected-registrar-wrapper>
|
||||
<div class="console-app-domains">
|
||||
<h1 class="mat-headline-4">Domains</h1>
|
||||
<h1 class="mat-headline-4" forceFocus>Domains</h1>
|
||||
|
||||
<div
|
||||
class="console-app-domains__actions-wrapper"
|
||||
@@ -24,11 +24,37 @@
|
||||
</div>
|
||||
} @else {
|
||||
<mat-menu #actions="matMenu">
|
||||
<ng-template matMenuContent let-domainName="domainName">
|
||||
<button mat-menu-item (click)="openRegistryLock(domainName)">
|
||||
<ng-template
|
||||
matMenuContent
|
||||
let-domainName="domainName"
|
||||
let-domain="domain"
|
||||
>
|
||||
<button
|
||||
mat-menu-item
|
||||
(click)="openRegistryLock(domainName)"
|
||||
aria-label="Access registry lock for domain"
|
||||
>
|
||||
<mat-icon>key</mat-icon>
|
||||
<span>Registry Lock</span>
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
(click)="onSuspendClick(domainName)"
|
||||
[elementId]="getElementIdForSuspendUnsuspend()"
|
||||
[disabled]="isDomainUnsuspendable(domain)"
|
||||
>
|
||||
<mat-icon>lock_clock</mat-icon>
|
||||
<span>Suspend</span>
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
(click)="onUnsuspendClick(domainName)"
|
||||
[elementId]="getElementIdForSuspendUnsuspend()"
|
||||
[disabled]="!isDomainUnsuspendable(domain)"
|
||||
>
|
||||
<mat-icon>lock_open</mat-icon>
|
||||
<span>Unsuspend</span>
|
||||
</button>
|
||||
</ng-template>
|
||||
</mat-menu>
|
||||
<div
|
||||
@@ -65,16 +91,67 @@
|
||||
/>
|
||||
</mat-form-field>
|
||||
|
||||
<div
|
||||
class="console-app__domains-selection"
|
||||
[elementId]="getElementIdForBulkDelete()"
|
||||
[ngClass]="{ active: selection.hasValue() }"
|
||||
>
|
||||
<div class="console-app__domains-selection-text">
|
||||
{{ selection.selected.length }} Selected
|
||||
</div>
|
||||
<div class="console-app__domains-selection-actions">
|
||||
<button
|
||||
mat-flat-button
|
||||
aria-label="Delete Selected Domains"
|
||||
[attr.aria-hidden]="!selection.hasValue()"
|
||||
(click)="deleteSelectedDomains()"
|
||||
>
|
||||
Delete Selected Domains
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<mat-table
|
||||
[dataSource]="dataSource"
|
||||
class="mat-elevation-z0"
|
||||
class="console-app__domains-table"
|
||||
>
|
||||
<!-- Checkbox Column -->
|
||||
<ng-container matColumnDef="select">
|
||||
<mat-header-cell *matHeaderCellDef>
|
||||
<mat-checkbox
|
||||
(change)="$event ? toggleAllRows() : null"
|
||||
[checked]="selection.hasValue() && isAllSelected"
|
||||
[indeterminate]="selection.hasValue() && !isAllSelected"
|
||||
[aria-label]="checkboxLabel()"
|
||||
[elementId]="getElementIdForBulkDelete()"
|
||||
>
|
||||
</mat-checkbox>
|
||||
</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">
|
||||
<mat-checkbox
|
||||
(click)="$event.stopPropagation()"
|
||||
(change)="$event ? selection.toggle(row) : null"
|
||||
[checked]="selection.isSelected(row)"
|
||||
[aria-label]="checkboxLabel(row)"
|
||||
[elementId]="getElementIdForBulkDelete()"
|
||||
>
|
||||
</mat-checkbox>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="domainName">
|
||||
<mat-header-cell *matHeaderCellDef>Domain Name</mat-header-cell>
|
||||
<mat-cell *matCellDef="let element">{{
|
||||
element.domainName
|
||||
}}</mat-cell>
|
||||
<mat-cell *matCellDef="let element">
|
||||
<mat-icon
|
||||
*ngIf="getOperationMessage(element.domainName)"
|
||||
[matTooltip]="getOperationMessage(element.domainName)"
|
||||
matTooltipPosition="above"
|
||||
class="primary-text"
|
||||
>info</mat-icon
|
||||
>
|
||||
<span>{{ element.domainName }}</span>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="creationTime">
|
||||
@@ -115,7 +192,10 @@
|
||||
<button
|
||||
mat-icon-button
|
||||
[matMenuTriggerFor]="actions"
|
||||
[matMenuTriggerData]="{ domainName: element.domainName }"
|
||||
[matMenuTriggerData]="{
|
||||
domainName: element.domainName,
|
||||
domain: element
|
||||
}"
|
||||
aria-label="Domain actions"
|
||||
>
|
||||
<mat-icon>more_horiz</mat-icon>
|
||||
|
||||
@@ -12,6 +12,22 @@
|
||||
}
|
||||
}
|
||||
|
||||
&__domains-selection {
|
||||
height: 60px;
|
||||
max-height: 0;
|
||||
transition: max-height 0.2s linear;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
gap: 20px;
|
||||
&-text {
|
||||
font-weight: bold;
|
||||
}
|
||||
&.active {
|
||||
max-height: 60px;
|
||||
}
|
||||
}
|
||||
|
||||
&-domains__download {
|
||||
position: absolute;
|
||||
top: -55px;
|
||||
@@ -41,6 +57,22 @@
|
||||
overflow: hidden;
|
||||
word-break: break-word;
|
||||
}
|
||||
.mat-column-select {
|
||||
max-width: 60px;
|
||||
padding-left: 15px;
|
||||
}
|
||||
.mat-column-domainName {
|
||||
position: relative;
|
||||
padding-left: 25px;
|
||||
mat-icon {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
mat-cell:has([style*="display: none"]),
|
||||
mat-header-cell:has([style*="display: none"]) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__domains-spinner {
|
||||
|
||||
@@ -21,6 +21,7 @@ import { MaterialModule } from '../material.module';
|
||||
import { BackendService } from '../shared/services/backend.service';
|
||||
import { DomainListComponent } from './domainList.component';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { AppModule } from '../app.module';
|
||||
|
||||
describe('DomainListComponent', () => {
|
||||
let component: DomainListComponent;
|
||||
@@ -29,7 +30,12 @@ describe('DomainListComponent', () => {
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [DomainListComponent],
|
||||
imports: [MaterialModule, BrowserAnimationsModule, FormsModule],
|
||||
imports: [
|
||||
MaterialModule,
|
||||
BrowserAnimationsModule,
|
||||
FormsModule,
|
||||
AppModule,
|
||||
],
|
||||
providers: [
|
||||
BackendService,
|
||||
provideHttpClient(),
|
||||
|
||||
@@ -12,27 +12,123 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { SelectionModel } from '@angular/cdk/collections';
|
||||
import { HttpErrorResponse, HttpStatusCode } from '@angular/common/http';
|
||||
import { Component, ViewChild, effect } from '@angular/core';
|
||||
import { Component, effect, Inject, ViewChild } from '@angular/core';
|
||||
import { MatPaginator, PageEvent } from '@angular/material/paginator';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { Subject, debounceTime } from 'rxjs';
|
||||
import { debounceTime, filter, Subject, take } from 'rxjs';
|
||||
import { RegistrarService } from '../registrar/registrar.service';
|
||||
import { Domain, DomainListService } from './domainList.service';
|
||||
import {
|
||||
BULK_ACTION_NAME,
|
||||
Domain,
|
||||
DomainListService,
|
||||
} from './domainList.service';
|
||||
import { RegistryLockComponent } from './registryLock.component';
|
||||
import { RegistryLockService } from './registryLock.service';
|
||||
import {
|
||||
MAT_DIALOG_DATA,
|
||||
MatDialog,
|
||||
MatDialogRef,
|
||||
} from '@angular/material/dialog';
|
||||
import { RESTRICTED_ELEMENTS } from '../shared/directives/userLevelVisiblity.directive';
|
||||
|
||||
interface DomainResponse {
|
||||
message: string;
|
||||
responseCode: string;
|
||||
}
|
||||
|
||||
interface DomainData {
|
||||
[domain: string]: DomainResponse;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-response-dialog',
|
||||
template: `
|
||||
<h2 mat-dialog-title>{{ data.title }}</h2>
|
||||
<mat-dialog-content [innerHTML]="data.content" />
|
||||
<mat-dialog-actions>
|
||||
<button mat-button (click)="onClose()">Close</button>
|
||||
</mat-dialog-actions>
|
||||
`,
|
||||
standalone: false,
|
||||
})
|
||||
export class ResponseDialogComponent {
|
||||
constructor(
|
||||
public dialogRef: MatDialogRef<ReasonDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA)
|
||||
public data: { title: string; content: string }
|
||||
) {}
|
||||
|
||||
onClose(): void {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
}
|
||||
|
||||
enum Operation {
|
||||
deleting = 'deleting',
|
||||
suspending = 'suspending',
|
||||
unsuspending = 'unsuspending',
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-reason-dialog',
|
||||
template: `
|
||||
<h2 mat-dialog-title>
|
||||
Please provide the (EPP) reason for {{ data.operation }} the domain(s):
|
||||
</h2>
|
||||
<mat-dialog-content>
|
||||
<mat-form-field appearance="outline" style="width:100%">
|
||||
<textarea matInput [(ngModel)]="reason" rows="4"></textarea>
|
||||
</mat-form-field>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions>
|
||||
<button mat-button (click)="onCancel()">Cancel</button>
|
||||
<button mat-button color="warn" (click)="onSave()" [disabled]="!reason">
|
||||
Save
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
`,
|
||||
standalone: false,
|
||||
})
|
||||
export class ReasonDialogComponent {
|
||||
reason: string = '';
|
||||
constructor(
|
||||
public dialogRef: MatDialogRef<ReasonDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA)
|
||||
public data: { operation: Operation }
|
||||
) {}
|
||||
|
||||
onSave(): void {
|
||||
this.dialogRef.close(this.reason);
|
||||
}
|
||||
|
||||
onCancel(): void {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-domain-list',
|
||||
templateUrl: './domainList.component.html',
|
||||
styleUrls: ['./domainList.component.scss'],
|
||||
standalone: false,
|
||||
})
|
||||
export class DomainListComponent {
|
||||
public static PATH = 'domain-list';
|
||||
private static SUSPENDED_STATUSES = [
|
||||
'SERVER_RENEW_PROHIBITED',
|
||||
'SERVER_TRANSFER_PROHIBITED',
|
||||
'SERVER_UPDATE_PROHIBITED',
|
||||
'SERVER_DELETE_PROHIBITED',
|
||||
'SERVER_HOLD',
|
||||
];
|
||||
private readonly DEBOUNCE_MS = 500;
|
||||
isAllSelected = false;
|
||||
|
||||
displayedColumns: string[] = [
|
||||
'select',
|
||||
'domainName',
|
||||
'creationTime',
|
||||
'registrationExpirationTime',
|
||||
@@ -42,6 +138,7 @@ export class DomainListComponent {
|
||||
];
|
||||
|
||||
dataSource: MatTableDataSource<Domain> = new MatTableDataSource();
|
||||
selection = new SelectionModel<Domain>(true, [], undefined, this.isChecked());
|
||||
isLoading = true;
|
||||
|
||||
searchTermSubject = new Subject<string>();
|
||||
@@ -51,13 +148,18 @@ export class DomainListComponent {
|
||||
resultsPerPage = 50;
|
||||
totalResults?: number = 0;
|
||||
|
||||
reason: string = '';
|
||||
|
||||
operationResult: DomainData | undefined;
|
||||
|
||||
@ViewChild(MatPaginator, { static: true }) paginator!: MatPaginator;
|
||||
|
||||
constructor(
|
||||
protected domainListService: DomainListService,
|
||||
protected registrarService: RegistrarService,
|
||||
protected registryLockService: RegistryLockService,
|
||||
private _snackBar: MatSnackBar
|
||||
private _snackBar: MatSnackBar,
|
||||
private dialog: MatDialog
|
||||
) {
|
||||
effect(() => {
|
||||
this.pageNumber = 0;
|
||||
@@ -134,6 +236,184 @@ export class DomainListComponent {
|
||||
onPageChange(event: PageEvent) {
|
||||
this.pageNumber = event.pageIndex;
|
||||
this.resultsPerPage = event.pageSize;
|
||||
this.selection.clear();
|
||||
this.reloadData();
|
||||
}
|
||||
|
||||
toggleAllRows() {
|
||||
if (this.isAllSelected) {
|
||||
this.selection.clear();
|
||||
this.isAllSelected = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.selection.select(...this.dataSource.data);
|
||||
this.isAllSelected = true;
|
||||
}
|
||||
|
||||
checkboxLabel(row?: Domain): string {
|
||||
if (!row) {
|
||||
return `${this.isAllSelected ? 'deselect' : 'select'} all`;
|
||||
}
|
||||
return `${this.selection.isSelected(row) ? 'deselect' : 'select'} row ${
|
||||
row.domainName
|
||||
}`;
|
||||
}
|
||||
|
||||
private isChecked(): ((o1: Domain, o2: Domain) => boolean) | undefined {
|
||||
return (o1: Domain, o2: Domain) => {
|
||||
if (!o1.domainName || !o2.domainName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.isAllSelected || o1.domainName === o2.domainName;
|
||||
};
|
||||
}
|
||||
|
||||
getElementIdForBulkDelete() {
|
||||
return RESTRICTED_ELEMENTS.BULK_DELETE;
|
||||
}
|
||||
|
||||
getElementIdForSuspendUnsuspend() {
|
||||
return RESTRICTED_ELEMENTS.SUSPEND;
|
||||
}
|
||||
|
||||
getOperationMessage(domain: string) {
|
||||
if (this.operationResult && this.operationResult[domain])
|
||||
return this.operationResult[domain].message;
|
||||
return '';
|
||||
}
|
||||
|
||||
isDomainUnsuspendable(domain: Domain) {
|
||||
return DomainListComponent.SUSPENDED_STATUSES.every((s) =>
|
||||
domain.statuses.includes(s)
|
||||
);
|
||||
}
|
||||
|
||||
sendDeleteRequest(reason: string) {
|
||||
this.isLoading = true;
|
||||
this.domainListService
|
||||
.bulkDomainAction(
|
||||
this.selection.selected.map((d) => d.domainName),
|
||||
reason,
|
||||
this.registrarService.registrarId(),
|
||||
BULK_ACTION_NAME.DELETE
|
||||
)
|
||||
.pipe(take(1))
|
||||
.subscribe({
|
||||
next: (result: DomainData) => {
|
||||
this.isLoading = false;
|
||||
const successCount = Object.keys(result).filter((domainName) =>
|
||||
result[domainName].responseCode.toString().startsWith('1')
|
||||
).length;
|
||||
const failureCount = Object.keys(result).length - successCount;
|
||||
this.dialog.open(ResponseDialogComponent, {
|
||||
data: {
|
||||
title: 'Domain Deletion Results',
|
||||
content: `Successfully deleted - ${successCount} domain(s)<br/>Failed to delete - ${failureCount} domain(s)<br/>${
|
||||
failureCount
|
||||
? 'Some domains could not be deleted due to ongoing processes or server errors. '
|
||||
: ''
|
||||
}Please check the table for more information.`,
|
||||
},
|
||||
});
|
||||
this.selection.clear();
|
||||
this.operationResult = result;
|
||||
this.reloadData();
|
||||
},
|
||||
error: (err: HttpErrorResponse) => {
|
||||
this.isLoading = false;
|
||||
this._snackBar.open(err.error || err.message);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
deleteSelectedDomains() {
|
||||
const dialogRef = this.dialog.open(ReasonDialogComponent, {
|
||||
data: {
|
||||
operation: Operation.deleting,
|
||||
},
|
||||
});
|
||||
|
||||
dialogRef
|
||||
.afterClosed()
|
||||
.pipe(
|
||||
take(1),
|
||||
filter((reason) => !!reason)
|
||||
)
|
||||
.subscribe(this.sendDeleteRequest.bind(this));
|
||||
}
|
||||
|
||||
sendSuspendUnsuspendRequest(
|
||||
domainName: string,
|
||||
reason: string,
|
||||
actionName: BULK_ACTION_NAME
|
||||
) {
|
||||
this.isLoading = true;
|
||||
this.domainListService
|
||||
.bulkDomainAction(
|
||||
[domainName],
|
||||
reason,
|
||||
this.registrarService.registrarId(),
|
||||
actionName
|
||||
)
|
||||
.pipe(take(1))
|
||||
.subscribe({
|
||||
next: (result: DomainData) => {
|
||||
this.isLoading = false;
|
||||
if (result[domainName].responseCode.toString().startsWith('2')) {
|
||||
this._snackBar.open(result[domainName].message);
|
||||
} else {
|
||||
this.reloadData();
|
||||
}
|
||||
},
|
||||
error: (err: HttpErrorResponse) => {
|
||||
this.isLoading = false;
|
||||
this._snackBar.open(err.error || err.message);
|
||||
},
|
||||
});
|
||||
}
|
||||
onSuspendClick(domainName: string) {
|
||||
const dialogRef = this.dialog.open(ReasonDialogComponent, {
|
||||
data: {
|
||||
operation: Operation.suspending,
|
||||
},
|
||||
});
|
||||
|
||||
dialogRef
|
||||
.afterClosed()
|
||||
.pipe(
|
||||
take(1),
|
||||
filter((reason) => !!reason)
|
||||
)
|
||||
.subscribe((reason) => {
|
||||
this.sendSuspendUnsuspendRequest(
|
||||
domainName,
|
||||
reason,
|
||||
BULK_ACTION_NAME.SUSPEND
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
onUnsuspendClick(domainName: string) {
|
||||
const dialogRef = this.dialog.open(ReasonDialogComponent, {
|
||||
data: {
|
||||
operation: Operation.unsuspending,
|
||||
},
|
||||
});
|
||||
|
||||
dialogRef
|
||||
.afterClosed()
|
||||
.pipe(
|
||||
take(1),
|
||||
filter((reason) => !!reason)
|
||||
)
|
||||
.subscribe((reason) => {
|
||||
this.sendSuspendUnsuspendRequest(
|
||||
domainName,
|
||||
reason,
|
||||
BULK_ACTION_NAME.UNSUSPEND
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,12 @@ export interface DomainListResult {
|
||||
totalResults: number;
|
||||
}
|
||||
|
||||
export enum BULK_ACTION_NAME {
|
||||
DELETE = 'DELETE',
|
||||
SUSPEND = 'SUSPEND',
|
||||
UNSUSPEND = 'UNSUSPEND',
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
@@ -48,7 +54,6 @@ export class DomainListService {
|
||||
private backendService: BackendService,
|
||||
private registrarService: RegistrarService
|
||||
) {}
|
||||
|
||||
retrieveDomains(
|
||||
pageNumber?: number,
|
||||
resultsPerPage?: number,
|
||||
@@ -71,4 +76,18 @@ export class DomainListService {
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
bulkDomainAction(
|
||||
domains: string[],
|
||||
reason: string,
|
||||
registrarId: string,
|
||||
actionName: BULK_ACTION_NAME
|
||||
) {
|
||||
return this.backendService.bulkDomainAction(
|
||||
domains,
|
||||
reason,
|
||||
actionName,
|
||||
registrarId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
color="primary"
|
||||
type="submit"
|
||||
[disabled]="!unlockDomain.valid"
|
||||
aria-label="Submit domain unlock request"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
@@ -73,6 +74,7 @@
|
||||
color="primary"
|
||||
type="submit"
|
||||
[disabled]="!lockDomain.valid"
|
||||
aria-label="Submit domain lock request"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
|
||||
@@ -25,6 +25,7 @@ import { RegistryLockService } from './registryLock.service';
|
||||
selector: 'app-registry-lock',
|
||||
templateUrl: './registryLock.component.html',
|
||||
styleUrls: ['./registryLock.component.scss'],
|
||||
standalone: false,
|
||||
})
|
||||
export class RegistryLockComponent {
|
||||
readonly isLocked = computed(() =>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<mat-toolbar>
|
||||
<button
|
||||
mat-icon-button
|
||||
aria-label="Open menu"
|
||||
aria-label="Open navigation menu"
|
||||
(click)="toggleNavPane()"
|
||||
*ngIf="breakpointObserver.isMobileView()"
|
||||
class="console-app__menu-btn"
|
||||
@@ -12,6 +12,7 @@
|
||||
<a
|
||||
[routerLink]="'/home'"
|
||||
routerLinkActive="active"
|
||||
aria-label="Google Registry logo"
|
||||
class="console-app__logo"
|
||||
>
|
||||
<svg
|
||||
|
||||
@@ -17,6 +17,12 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { HeaderComponent } from './header.component';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { MaterialModule } from '../material.module';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { AppModule, SelectedRegistrarModule } from '../app.module';
|
||||
import { AppRoutingModule } from '../app-routing.module';
|
||||
import { BackendService } from '../shared/services/backend.service';
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
import { provideHttpClientTesting } from '@angular/common/http/testing';
|
||||
|
||||
describe('HeaderComponent', () => {
|
||||
let component: HeaderComponent;
|
||||
@@ -24,7 +30,19 @@ describe('HeaderComponent', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [MaterialModule, BrowserAnimationsModule],
|
||||
imports: [
|
||||
SelectedRegistrarModule,
|
||||
MaterialModule,
|
||||
BrowserAnimationsModule,
|
||||
AppRoutingModule,
|
||||
AppModule,
|
||||
],
|
||||
providers: [
|
||||
BackendService,
|
||||
{ provide: ActivatedRoute, useValue: {} as ActivatedRoute },
|
||||
provideHttpClient(),
|
||||
provideHttpClientTesting(),
|
||||
],
|
||||
declarations: [HeaderComponent],
|
||||
}).compileComponents();
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import { BreakPointObserverService } from '../shared/services/breakPoint.service
|
||||
selector: 'app-header',
|
||||
templateUrl: './header.component.html',
|
||||
styleUrls: ['./header.component.scss'],
|
||||
standalone: false,
|
||||
})
|
||||
export class HeaderComponent {
|
||||
private isNavOpen = false;
|
||||
|
||||
62
console-webapp/src/app/history/history.component.html
Normal file
62
console-webapp/src/app/history/history.component.html
Normal file
@@ -0,0 +1,62 @@
|
||||
<app-selected-registrar-wrapper>
|
||||
<div class="history-log">
|
||||
<h1 class="mat-headline-4" forceFocus>
|
||||
Registrar Console Activity History
|
||||
</h1>
|
||||
<mat-tab-group
|
||||
[elementId]="getElementIdForUserLog()"
|
||||
class="history-log__tabs"
|
||||
>
|
||||
<mat-tab label="Registrar Activity">
|
||||
<div class="spacer"></div>
|
||||
|
||||
<app-history-list
|
||||
[historyRecords]="historyService.historyRecordsRegistrar()"
|
||||
[isLoading]="isLoading"
|
||||
/>
|
||||
</mat-tab>
|
||||
<mat-tab label="User Activity">
|
||||
<div class="spacer"></div>
|
||||
<form (ngSubmit)="loadHistory()" #form="ngForm">
|
||||
<section>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Console User Email: </mat-label>
|
||||
<input
|
||||
matInput
|
||||
id="email"
|
||||
type="email"
|
||||
name="consoleUserEmail"
|
||||
required
|
||||
email
|
||||
[(ngModel)]="consoleUserEmail"
|
||||
#emailControl="ngModel"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</section>
|
||||
<div class="spacer"></div>
|
||||
<button
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
type="submit"
|
||||
aria-label="Search user history"
|
||||
[disabled]="!form.valid"
|
||||
>
|
||||
Search
|
||||
</button>
|
||||
</form>
|
||||
<div class="spacer"></div>
|
||||
<app-history-list
|
||||
[historyRecords]="historyService.historyRecordsUser()"
|
||||
[isLoading]="isLoading"
|
||||
/>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
</div>
|
||||
|
||||
<app-history-list
|
||||
[elementId]="getElementIdForUserLog()"
|
||||
[isReverse]="true"
|
||||
[historyRecords]="historyService.historyRecordsUser()"
|
||||
[isLoading]="isLoading"
|
||||
/>
|
||||
</app-selected-registrar-wrapper>
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
// Copyright 2025 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -12,5 +12,11 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
@javax.annotation.ParametersAreNonnullByDefault
|
||||
package google.registry.module.frontend;
|
||||
.history-log {
|
||||
font-family: "Roboto", sans-serif;
|
||||
max-width: 760px;
|
||||
|
||||
.spacer {
|
||||
margin: 20px 0;
|
||||
}
|
||||
}
|
||||
80
console-webapp/src/app/history/history.component.ts
Normal file
80
console-webapp/src/app/history/history.component.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
// Copyright 2025 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, effect } from '@angular/core';
|
||||
import { UserDataService } from '../shared/services/userData.service';
|
||||
import { BackendService } from '../shared/services/backend.service';
|
||||
import { RegistrarService } from '../registrar/registrar.service';
|
||||
import { HistoryService } from './history.service';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import {
|
||||
GlobalLoader,
|
||||
GlobalLoaderService,
|
||||
} from '../shared/services/globalLoader.service';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { RESTRICTED_ELEMENTS } from '../shared/directives/userLevelVisiblity.directive';
|
||||
|
||||
@Component({
|
||||
selector: 'app-history',
|
||||
templateUrl: './history.component.html',
|
||||
styleUrls: ['./history.component.scss'],
|
||||
providers: [HistoryService],
|
||||
standalone: false,
|
||||
})
|
||||
export class HistoryComponent implements GlobalLoader {
|
||||
public static PATH = 'history';
|
||||
|
||||
consoleUserEmail: string = '';
|
||||
isLoading: boolean = false;
|
||||
|
||||
constructor(
|
||||
private backendService: BackendService,
|
||||
private registrarService: RegistrarService,
|
||||
protected historyService: HistoryService,
|
||||
protected globalLoader: GlobalLoaderService,
|
||||
protected userDataService: UserDataService,
|
||||
private _snackBar: MatSnackBar
|
||||
) {
|
||||
effect(() => {
|
||||
if (registrarService.registrarId()) {
|
||||
this.loadHistory();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getElementIdForUserLog() {
|
||||
return RESTRICTED_ELEMENTS.ACTIVITY_PER_USER;
|
||||
}
|
||||
|
||||
loadingTimeout() {
|
||||
this._snackBar.open('Timeout loading records history');
|
||||
}
|
||||
|
||||
loadHistory() {
|
||||
this.globalLoader.startGlobalLoader(this);
|
||||
this.isLoading = true;
|
||||
this.historyService
|
||||
.getHistoryLog(this.registrarService.registrarId(), this.consoleUserEmail)
|
||||
.subscribe({
|
||||
error: (err: HttpErrorResponse) => {
|
||||
this._snackBar.open(err.error || err.message);
|
||||
this.isLoading = false;
|
||||
},
|
||||
next: () => {
|
||||
this.globalLoader.stopGlobalLoader(this);
|
||||
this.isLoading = false;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
46
console-webapp/src/app/history/history.service.ts
Normal file
46
console-webapp/src/app/history/history.service.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
// Copyright 2025 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Injectable, signal } from '@angular/core';
|
||||
import { BackendService } from '../shared/services/backend.service';
|
||||
import { tap } from 'rxjs';
|
||||
|
||||
export interface HistoryRecord {
|
||||
modificationTime: string;
|
||||
type: string;
|
||||
description: string;
|
||||
actingUser: {
|
||||
emailAddress: string;
|
||||
};
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class HistoryService {
|
||||
historyRecordsRegistrar = signal<HistoryRecord[]>([]);
|
||||
historyRecordsUser = signal<HistoryRecord[]>([]);
|
||||
|
||||
constructor(private backendService: BackendService) {}
|
||||
|
||||
getHistoryLog(registrarId: string, userEmail?: string) {
|
||||
return this.backendService.getHistoryLog(registrarId, userEmail).pipe(
|
||||
tap((historyRecords: HistoryRecord[]) => {
|
||||
if (userEmail) {
|
||||
this.historyRecordsUser.set(historyRecords);
|
||||
} else {
|
||||
this.historyRecordsRegistrar.set(historyRecords);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
50
console-webapp/src/app/history/historyList.component.html
Normal file
50
console-webapp/src/app/history/historyList.component.html
Normal file
@@ -0,0 +1,50 @@
|
||||
@if (!isLoading && historyRecords.length == 0) {
|
||||
<div class="history-list__no-records">
|
||||
<mat-icon class="history-list__no-records-icon secondary-text"
|
||||
>apps_outage</mat-icon
|
||||
>
|
||||
<h1>No records found</h1>
|
||||
</div>
|
||||
} @else {
|
||||
<mat-card>
|
||||
<mat-card-content>
|
||||
<mat-list role="list">
|
||||
<ng-container *ngFor="let item of historyRecords; let last = last">
|
||||
<mat-list-item class="history-list__item">
|
||||
<mat-icon
|
||||
[ngClass]="getIconClass(item.type)"
|
||||
class="history-list__icon"
|
||||
>
|
||||
{{ getIconForType(item.type) }}
|
||||
</mat-icon>
|
||||
|
||||
<div class="history-list__content">
|
||||
<div class="history-list__description">
|
||||
<span class="history-list__description--main">{{
|
||||
item.type
|
||||
}}</span>
|
||||
<div>
|
||||
<mat-chip
|
||||
*ngIf="parseDescription(item.description).detail"
|
||||
class="history-list__chip"
|
||||
>
|
||||
{{ parseDescription(item.description).detail }}
|
||||
</mat-chip>
|
||||
</div>
|
||||
</div>
|
||||
<div class="history-list__user">
|
||||
<b>User - {{ item.actingUser.emailAddress }}</b>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span class="history-list__timestamp">
|
||||
{{ item.modificationTime | date : "MMM d, y, h:mm a" }}
|
||||
</span>
|
||||
</mat-list-item>
|
||||
|
||||
<mat-divider *ngIf="!last"></mat-divider>
|
||||
</ng-container>
|
||||
</mat-list>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
}
|
||||
81
console-webapp/src/app/history/historyList.component.scss
Normal file
81
console-webapp/src/app/history/historyList.component.scss
Normal file
@@ -0,0 +1,81 @@
|
||||
// Copyright 2025 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
.history-list {
|
||||
font-family: "Roboto", sans-serif;
|
||||
&__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
// Override default mat-list-item height to fit content
|
||||
height: auto !important;
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
&__no-records {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
&__no-records-icon {
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
font-size: 4rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
margin-right: 16px;
|
||||
|
||||
&--update {
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
&--security {
|
||||
color: #d32f2f;
|
||||
}
|
||||
}
|
||||
|
||||
&__description {
|
||||
&--main {
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
color: rgba(0, 0, 0, 0.87);
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
&__chip {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
&__user {
|
||||
font-size: 0.9rem;
|
||||
color: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
&__timestamp {
|
||||
color: rgba(0, 0, 0, 0.6);
|
||||
white-space: nowrap;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
66
console-webapp/src/app/history/historyList.component.ts
Normal file
66
console-webapp/src/app/history/historyList.component.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
// Copyright 2025 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
||||
import { HistoryRecord } from './history.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-history-list',
|
||||
templateUrl: './historyList.component.html',
|
||||
styleUrls: ['./historyList.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: false,
|
||||
})
|
||||
export class HistoryListComponent {
|
||||
@Input() historyRecords: HistoryRecord[] = [];
|
||||
@Input() isLoading: boolean = false;
|
||||
|
||||
getIconForType(type: string): string {
|
||||
switch (type) {
|
||||
case 'REGISTRAR_UPDATE':
|
||||
return 'edit';
|
||||
case 'REGISTRAR_SECURITY_UPDATE':
|
||||
return 'security';
|
||||
default:
|
||||
return 'history'; // A fallback icon
|
||||
}
|
||||
}
|
||||
|
||||
getIconClass(type: string): string {
|
||||
switch (type) {
|
||||
case 'REGISTRAR_UPDATE':
|
||||
return 'history-log__icon--update';
|
||||
case 'REGISTRAR_SECURITY_UPDATE':
|
||||
return 'history-log__icon--security';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
parseDescription(description: string): {
|
||||
main: string;
|
||||
detail: string | null;
|
||||
} {
|
||||
if (!description) {
|
||||
return { main: 'N/A', detail: null };
|
||||
}
|
||||
const parts = description.split('|');
|
||||
const detail = parts.length > 1 ? parts[1].replace(/_/g, ' ') : parts[0];
|
||||
|
||||
return {
|
||||
main: parts[0],
|
||||
detail: detail,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { HomeComponent } from './home.component';
|
||||
import { MaterialModule } from '../material.module';
|
||||
import { AppModule } from '../app.module';
|
||||
|
||||
describe('HomeComponent', () => {
|
||||
let component: HomeComponent;
|
||||
@@ -23,7 +24,7 @@ describe('HomeComponent', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [MaterialModule],
|
||||
imports: [MaterialModule, AppModule],
|
||||
declarations: [HomeComponent],
|
||||
}).compileComponents();
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import { BreakPointObserverService } from '../shared/services/breakPoint.service
|
||||
selector: 'app-home',
|
||||
templateUrl: './home.component.html',
|
||||
styleUrls: ['./home.component.scss'],
|
||||
standalone: false,
|
||||
})
|
||||
export class HomeComponent {
|
||||
constructor(
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
margin-top: 30px;
|
||||
}
|
||||
&-subhead {
|
||||
font-size: 20px;
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import { DomainListComponent } from '../domains/domainList.component';
|
||||
templateUrl: './registryLockVerify.component.html',
|
||||
styleUrls: ['./registryLockVerify.component.scss'],
|
||||
providers: [RegistryLockVerifyService],
|
||||
standalone: false,
|
||||
})
|
||||
export class RegistryLockVerifyComponent {
|
||||
public static PATH = 'registry-lock-verify';
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
<mat-tree-node
|
||||
*matTreeNodeDef="let node"
|
||||
matTreeNodeToggle
|
||||
tabindex="0"
|
||||
(click)="onClick(node)"
|
||||
(keyup.enter)="onClick(node)"
|
||||
[class.active]="router.url.includes(node.path)"
|
||||
[elementId]="getElementId(node)"
|
||||
>
|
||||
@@ -18,6 +20,8 @@
|
||||
<mat-nested-tree-node
|
||||
*matTreeNodeDef="let node; when: hasChild"
|
||||
(click)="onClick(node)"
|
||||
tabindex="0"
|
||||
(keyup.enter)="onClick(node)"
|
||||
>
|
||||
<div class="mat-tree-node" [class.active]="router.url.includes(node.path)">
|
||||
<button
|
||||
|
||||
@@ -33,6 +33,7 @@ interface NavMenuNode extends RouteWithIcon {
|
||||
selector: 'app-navigation',
|
||||
templateUrl: './navigation.component.html',
|
||||
styleUrls: ['./navigation.component.scss'],
|
||||
standalone: false,
|
||||
})
|
||||
export class NavigationComponent {
|
||||
renderRouter: boolean = true;
|
||||
@@ -56,7 +57,7 @@ export class NavigationComponent {
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.subscription.unsubscribe();
|
||||
this.subscription && this.subscription.unsubscribe();
|
||||
}
|
||||
|
||||
getElementId(node: RouteWithIcon) {
|
||||
|
||||
@@ -30,7 +30,14 @@
|
||||
>
|
||||
</mat-form-field>
|
||||
</p>
|
||||
<button mat-flat-button color="primary" type="submit">Save</button>
|
||||
<button
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
type="submit"
|
||||
aria-label="Submit new OT&E account"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</form>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -26,7 +26,6 @@ export interface OteCreateResponse extends Map<string, string> {
|
||||
|
||||
@Component({
|
||||
selector: 'app-ote',
|
||||
standalone: true,
|
||||
imports: [MaterialModule, SnackBarModule],
|
||||
templateUrl: './newOte.component.html',
|
||||
styleUrls: ['./newOte.component.scss'],
|
||||
|
||||
@@ -31,7 +31,6 @@ export interface OteStatusResponse {
|
||||
|
||||
@Component({
|
||||
selector: 'app-ote-status',
|
||||
standalone: true,
|
||||
imports: [MaterialModule, SnackBarModule, CommonModule],
|
||||
templateUrl: './oteStatus.component.html',
|
||||
styleUrls: ['./oteStatus.component.scss'],
|
||||
|
||||
@@ -175,6 +175,7 @@ JPY=billing-id-for-yen"
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
type="submit"
|
||||
aria-label="Submit new registrar request"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
|
||||
@@ -33,6 +33,7 @@ interface LocalizedAddressStreet {
|
||||
templateUrl: './newRegistrar.component.html',
|
||||
styleUrls: ['./newRegistrar.component.scss'],
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
standalone: false,
|
||||
})
|
||||
export default class NewRegistrarComponent {
|
||||
protected newRegistrar: Registrar;
|
||||
@@ -47,7 +48,6 @@ export default class NewRegistrarComponent {
|
||||
this.newRegistrar = {
|
||||
registrarId: '',
|
||||
url: '',
|
||||
whoisServer: '',
|
||||
registrarName: '',
|
||||
icannReferralEmail: '',
|
||||
localizedAddress: {
|
||||
|
||||
@@ -50,17 +50,16 @@ export interface SecuritySettings
|
||||
ipAddressAllowList?: Array<IpAllowListItem>;
|
||||
}
|
||||
|
||||
export interface WhoisRegistrarFields {
|
||||
export interface RdapRegistrarFields {
|
||||
ianaIdentifier?: number;
|
||||
icannReferralEmail: string;
|
||||
localizedAddress: Address;
|
||||
registrarId: string;
|
||||
url: string;
|
||||
whoisServer: string;
|
||||
}
|
||||
|
||||
export interface Registrar
|
||||
extends WhoisRegistrarFields,
|
||||
extends RdapRegistrarFields,
|
||||
SecuritySettingsBackendModel {
|
||||
allowedTlds?: string[];
|
||||
billingAccountMap?: object;
|
||||
@@ -72,6 +71,7 @@ export interface Registrar
|
||||
registrarName: string;
|
||||
registryLockAllowed?: boolean;
|
||||
type?: string;
|
||||
lastPocVerificationDate?: string;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
<div class="console-app__registrar-view">
|
||||
<div
|
||||
class="console-app__registrar-view"
|
||||
cdkTrapFocus
|
||||
[cdkTrapFocusAutoCapture]="true"
|
||||
>
|
||||
<h1 class="mat-headline-4">Registrars</h1>
|
||||
<mat-divider></mat-divider>
|
||||
<div class="console-app__registrar-view-content">
|
||||
@@ -12,7 +16,7 @@
|
||||
*ngIf="oteButtonVisible"
|
||||
mat-stroked-button
|
||||
(click)="checkOteStatus()"
|
||||
aria-label="Check OT&E account"
|
||||
aria-label="Check OT&E account status"
|
||||
[elementId]="getElementIdForOteBlock()"
|
||||
>
|
||||
Check OT&E Status
|
||||
|
||||
@@ -27,6 +27,7 @@ import { environment } from '../../environments/environment';
|
||||
selector: 'app-registrar-details',
|
||||
templateUrl: './registrarDetails.component.html',
|
||||
styleUrls: ['./registrarDetails.component.scss'],
|
||||
standalone: false,
|
||||
})
|
||||
export class RegistrarDetailsComponent implements OnInit {
|
||||
public static PATH = 'registrars/:id';
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
(focus)="onFocus()"
|
||||
[matAutocomplete]="auto"
|
||||
spellcheck="false"
|
||||
/>
|
||||
<mat-autocomplete
|
||||
autoActiveFirstOption
|
||||
|
||||
@@ -19,12 +19,16 @@ import { RegistrarService } from './registrar.service';
|
||||
selector: 'app-registrar-selector',
|
||||
templateUrl: './registrarSelector.component.html',
|
||||
styleUrls: ['./registrarSelector.component.scss'],
|
||||
standalone: false,
|
||||
})
|
||||
export class RegistrarSelectorComponent {
|
||||
registrarInput = signal<string>(this.registrarService.registrarId());
|
||||
filteredOptions?: string[];
|
||||
allRegistrarIds = computed(() =>
|
||||
this.registrarService.registrars().map((r) => r.registrarId)
|
||||
this.registrarService
|
||||
.registrars()
|
||||
.map((r) => r.registrarId)
|
||||
.sort()
|
||||
);
|
||||
|
||||
constructor(protected registrarService: RegistrarService) {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
} @else {
|
||||
<div class="console-app__registrars">
|
||||
<div class="console-app__registrars-header">
|
||||
<h1 class="mat-headline-4">Registrars</h1>
|
||||
<h1 class="mat-headline-4" forceFocus>Registrars</h1>
|
||||
<div class="spacer"></div>
|
||||
<button
|
||||
mat-stroked-button
|
||||
@@ -59,6 +59,8 @@
|
||||
<mat-row
|
||||
*matRowDef="let row; columns: displayedColumns"
|
||||
(click)="openDetails(row.registrarId)"
|
||||
tabindex="0"
|
||||
(keyup.enter)="openDetails(row.registrarId)"
|
||||
></mat-row>
|
||||
</mat-table>
|
||||
|
||||
|
||||
@@ -78,6 +78,7 @@ export const columns = [
|
||||
templateUrl: './registrarsTable.component.html',
|
||||
styleUrls: ['./registrarsTable.component.scss'],
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
standalone: false,
|
||||
})
|
||||
export class RegistrarComponent {
|
||||
public static PATH = 'registrars';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<h1 class="mat-headline-4">Resources</h1>
|
||||
<h1 class="mat-headline-4" forceFocus>Resources</h1>
|
||||
<div class="console-app__resources">
|
||||
<div>
|
||||
<div class="console-app__resources-subhead">Technical resources</div>
|
||||
@@ -11,6 +11,6 @@
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<img src="./assets/resources.png" />
|
||||
<img src="./assets/resources.png" alt="Generic resources image" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
width: 100%;
|
||||
}
|
||||
&-subhead {
|
||||
font-size: 20px;
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import { UserDataService } from '../shared/services/userData.service';
|
||||
selector: 'app-resources',
|
||||
templateUrl: './resources.component.html',
|
||||
styleUrls: ['./resources.component.scss'],
|
||||
standalone: false,
|
||||
})
|
||||
export class ResourcesComponent {
|
||||
public static PATH = 'resources';
|
||||
|
||||
@@ -32,7 +32,9 @@
|
||||
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
|
||||
<mat-row
|
||||
*matRowDef="let row; columns: displayedColumns"
|
||||
tabindex="0"
|
||||
(click)="openDetails(row)"
|
||||
(keyup.enter)="openDetails(row)"
|
||||
></mat-row>
|
||||
</mat-table>
|
||||
}
|
||||
|
||||
@@ -16,17 +16,14 @@ 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 {
|
||||
ContactService,
|
||||
contactTypeToViewReadyContact,
|
||||
ViewReadyContact,
|
||||
} from './contact.service';
|
||||
import { ContactService, ViewReadyContact } from './contact.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-contact',
|
||||
templateUrl: './contact.component.html',
|
||||
styleUrls: ['./contact.component.scss'],
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
standalone: false,
|
||||
})
|
||||
export default class ContactComponent {
|
||||
public static PATH = 'contact';
|
||||
|
||||
@@ -35,7 +35,7 @@ export const contactTypeToTextMap: contactTypesToUserFriendlyTypes = {
|
||||
LEGAL: 'Legal contact',
|
||||
MARKETING: 'Marketing contact',
|
||||
TECH: 'Technical contact',
|
||||
WHOIS: 'WHOIS-Inquiry contact',
|
||||
WHOIS: 'RDAP-Inquiry contact',
|
||||
};
|
||||
|
||||
type UserFriendlyType = (typeof contactTypeToTextMap)[contactType];
|
||||
@@ -47,9 +47,9 @@ export interface Contact {
|
||||
registrarId?: string;
|
||||
faxNumber?: string;
|
||||
types: Array<contactType>;
|
||||
visibleInWhoisAsAdmin?: boolean;
|
||||
visibleInWhoisAsTech?: boolean;
|
||||
visibleInDomainWhoisAsAbuse?: boolean;
|
||||
visibleInRdapAsAdmin?: boolean;
|
||||
visibleInRdapAsTech?: boolean;
|
||||
visibleInDomainRdapAsAbuse?: boolean;
|
||||
}
|
||||
|
||||
export interface ViewReadyContact extends Contact {
|
||||
@@ -59,7 +59,10 @@ export interface ViewReadyContact extends Contact {
|
||||
export function contactTypeToViewReadyContact(c: Contact): ViewReadyContact {
|
||||
return {
|
||||
...c,
|
||||
userFriendlyTypes: c.types?.map((cType) => contactTypeToTextMap[cType]),
|
||||
userFriendlyTypes: (c.types || []).map(
|
||||
(cType) => contactTypeToTextMap[cType]
|
||||
),
|
||||
types: c.types || [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -83,7 +86,7 @@ export class ContactService {
|
||||
: contactTypeToViewReadyContact({
|
||||
emailAddress: '',
|
||||
name: '',
|
||||
types: ['ADMIN'],
|
||||
types: ['TECH'],
|
||||
faxNumber: '',
|
||||
phoneNumber: '',
|
||||
registrarId: '',
|
||||
@@ -98,19 +101,21 @@ export class ContactService {
|
||||
);
|
||||
}
|
||||
|
||||
saveContacts(contacts: ViewReadyContact[]): Observable<Contact[]> {
|
||||
updateContact(contact: ViewReadyContact) {
|
||||
return this.backend
|
||||
.postContacts(this.registrarService.registrarId(), contacts)
|
||||
.updateContact(this.registrarService.registrarId(), contact)
|
||||
.pipe(switchMap((_) => this.fetchContacts()));
|
||||
}
|
||||
|
||||
addContact(contact: ViewReadyContact) {
|
||||
const newContacts = this.contacts().concat([contact]);
|
||||
return this.saveContacts(newContacts);
|
||||
return this.backend
|
||||
.createContact(this.registrarService.registrarId(), contact)
|
||||
.pipe(switchMap((_) => this.fetchContacts()));
|
||||
}
|
||||
|
||||
deleteContact(contact: ViewReadyContact) {
|
||||
const newContacts = this.contacts().filter((c) => c !== contact);
|
||||
return this.saveContacts(newContacts);
|
||||
return this.backend
|
||||
.deleteContact(this.registrarService.registrarId(), contact)
|
||||
.pipe(switchMap((_) => this.fetchContacts()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
<div class="console-app__contact" *ngIf="contactService.contactInEdit">
|
||||
<div
|
||||
class="console-app__contact"
|
||||
*ngIf="contactService.contactInEdit"
|
||||
cdkTrapFocus
|
||||
[cdkTrapFocusAutoCapture]="true"
|
||||
>
|
||||
<div class="console-app__contact-controls">
|
||||
<button
|
||||
mat-icon-button
|
||||
@@ -51,6 +56,12 @@
|
||||
[required]="true"
|
||||
[(ngModel)]="contactService.contactInEdit.emailAddress"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
[disabled]="emailAddressIsDisabled()"
|
||||
[matTooltip]="
|
||||
emailAddressIsDisabled()
|
||||
? 'Reach out to registry customer support to update email address'
|
||||
: ''
|
||||
"
|
||||
/>
|
||||
</mat-form-field>
|
||||
|
||||
@@ -78,43 +89,48 @@
|
||||
<h1>Contact Type</h1>
|
||||
<p class="console-app__contact-required">
|
||||
<mat-icon color="accent">error</mat-icon>Required to select at least one
|
||||
(primary contact can't be updated)
|
||||
</p>
|
||||
<div class="">
|
||||
<mat-checkbox
|
||||
<ng-container
|
||||
*ngFor="let contactType of contactTypeToTextMap | keyvalue"
|
||||
[checked]="checkboxIsChecked(contactType.key)"
|
||||
(change)="checkboxOnChange($event, contactType.key)"
|
||||
[disabled]="checkboxIsDisabled(contactType.key)"
|
||||
>
|
||||
{{ contactType.value }}
|
||||
</mat-checkbox>
|
||||
<mat-checkbox
|
||||
*ngIf="shouldDisplayCheckbox(contactType.key)"
|
||||
[checked]="checkboxIsChecked(contactType.key)"
|
||||
(change)="checkboxOnChange($event, contactType.key)"
|
||||
[disabled]="checkboxIsDisabled(contactType.key)"
|
||||
>
|
||||
{{ contactType.value }}
|
||||
</mat-checkbox>
|
||||
</ng-container>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h1>WHOIS Preferences</h1>
|
||||
<h1>RDAP Preferences</h1>
|
||||
<div>
|
||||
<mat-checkbox
|
||||
[(ngModel)]="contactService.contactInEdit.visibleInWhoisAsAdmin"
|
||||
[(ngModel)]="contactService.contactInEdit.visibleInRdapAsAdmin"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
>Show in Registrar WHOIS record as admin contact</mat-checkbox
|
||||
>Show in Registrar RDAP record as admin contact</mat-checkbox
|
||||
>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<mat-checkbox
|
||||
[(ngModel)]="contactService.contactInEdit.visibleInWhoisAsTech"
|
||||
[(ngModel)]="contactService.contactInEdit.visibleInRdapAsTech"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
>Show in Registrar WHOIS record as technical contact</mat-checkbox
|
||||
>Show in Registrar RDAP record as technical contact</mat-checkbox
|
||||
>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<mat-checkbox
|
||||
[(ngModel)]="contactService.contactInEdit.visibleInDomainWhoisAsAbuse"
|
||||
[(ngModel)]="contactService.contactInEdit.visibleInDomainRdapAsAbuse"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
>Show Phone and Email in Domain WHOIS Record as registrar abuse
|
||||
contact (per CL&D requirements)</mat-checkbox
|
||||
>Show Phone and Email in Domain RDAP Record as registrar abuse contact
|
||||
(per CL&D requirements)</mat-checkbox
|
||||
>
|
||||
</div>
|
||||
</section>
|
||||
@@ -123,6 +139,7 @@
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
type="submit"
|
||||
aria-label="Save contact updates"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
@@ -170,30 +187,30 @@
|
||||
<mat-card-content>
|
||||
<mat-list role="list">
|
||||
<mat-list-item role="listitem">
|
||||
<h2>WHOIS Preferences</h2>
|
||||
<h2>RDAP Preferences</h2>
|
||||
</mat-list-item>
|
||||
@if(contactService.contactInEdit.visibleInWhoisAsAdmin) {
|
||||
@if(contactService.contactInEdit.visibleInRdapAsAdmin) {
|
||||
<mat-divider></mat-divider>
|
||||
<mat-list-item role="listitem">
|
||||
<span class="console-app__list-value"
|
||||
>Show in Registrar WHOIS record as admin contact</span
|
||||
>Show in Registrar RDAP record as admin contact</span
|
||||
>
|
||||
</mat-list-item>
|
||||
} @if(contactService.contactInEdit.visibleInWhoisAsTech) {
|
||||
} @if(contactService.contactInEdit.visibleInRdapAsTech) {
|
||||
<mat-divider></mat-divider>
|
||||
<mat-list-item
|
||||
role="listitem"
|
||||
*ngIf="contactService.contactInEdit.visibleInWhoisAsTech"
|
||||
*ngIf="contactService.contactInEdit.visibleInRdapAsTech"
|
||||
>
|
||||
<span class="console-app__list-value"
|
||||
>Show in Registrar WHOIS record as technical contact</span
|
||||
>Show in Registrar RDAP record as technical contact</span
|
||||
>
|
||||
</mat-list-item>
|
||||
} @if(contactService.contactInEdit.visibleInDomainWhoisAsAbuse) {
|
||||
} @if(contactService.contactInEdit.visibleInDomainRdapAsAbuse) {
|
||||
<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
|
||||
>Show Phone and Email in Domain RDAP Record as registrar abuse
|
||||
contact (per CL&D requirements)</span
|
||||
>
|
||||
</mat-list-item>
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
selector: 'app-contact-details',
|
||||
templateUrl: './contactDetails.component.html',
|
||||
styleUrls: ['./contactDetails.component.scss'],
|
||||
standalone: false,
|
||||
})
|
||||
export class ContactDetailsComponent {
|
||||
protected contactTypeToTextMap = contactTypeToTextMap;
|
||||
@@ -68,9 +69,13 @@ export class ContactDetailsComponent {
|
||||
|
||||
save(e: SubmitEvent) {
|
||||
e.preventDefault();
|
||||
if ((this.contactService.contactInEdit.types || []).length === 0) {
|
||||
this._snackBar.open('Required to select contact type');
|
||||
return;
|
||||
}
|
||||
const request = this.contactService.isContactNewView
|
||||
? this.contactService.addContact(this.contactService.contactInEdit)
|
||||
: this.contactService.saveContacts(this.contactService.contacts());
|
||||
: this.contactService.updateContact(this.contactService.contactInEdit);
|
||||
request.subscribe({
|
||||
complete: () => {
|
||||
this.goBack();
|
||||
@@ -81,6 +86,10 @@ export class ContactDetailsComponent {
|
||||
});
|
||||
}
|
||||
|
||||
shouldDisplayCheckbox(type: string) {
|
||||
return type !== 'ADMIN' || this.checkboxIsChecked(type);
|
||||
}
|
||||
|
||||
checkboxIsChecked(type: string) {
|
||||
return this.contactService.contactInEdit.types.includes(
|
||||
type as contactType
|
||||
@@ -88,6 +97,9 @@ export class ContactDetailsComponent {
|
||||
}
|
||||
|
||||
checkboxIsDisabled(type: string) {
|
||||
if (type === 'ADMIN') {
|
||||
return true;
|
||||
}
|
||||
return (
|
||||
this.contactService.contactInEdit.types.length === 1 &&
|
||||
this.contactService.contactInEdit.types[0] === (type as contactType)
|
||||
@@ -104,4 +116,8 @@ export class ContactDetailsComponent {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
emailAddressIsDisabled() {
|
||||
return this.contactService.contactInEdit.types.includes('ADMIN');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
@if(whoisService.editing) {
|
||||
<app-whois-edit></app-whois-edit>
|
||||
@if(rdapService.editing) {
|
||||
<app-rdap-edit></app-rdap-edit>
|
||||
} @else {
|
||||
<div class="console-app__whois">
|
||||
<div class="console-app__whois-controls">
|
||||
<div class="console-app__rdap">
|
||||
<div class="console-app__rdap-controls">
|
||||
<span>
|
||||
General registrar information for your WHOIS record. This information is
|
||||
always visible in WHOIS.
|
||||
General registrar information for your RDAP record. This information is
|
||||
always visible in RDAP.
|
||||
</span>
|
||||
<div class="spacer"></div>
|
||||
<button
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
aria-label="Edit WHOIS record"
|
||||
(click)="whoisService.editing = true"
|
||||
aria-label="Edit RDAP record"
|
||||
(click)="rdapService.editing = true"
|
||||
>
|
||||
<mat-icon>edit</mat-icon>
|
||||
Edit
|
||||
@@ -61,45 +61,5 @@
|
||||
</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,4 +1,4 @@
|
||||
.console-app__whois {
|
||||
.console-app__rdap {
|
||||
max-width: 616px;
|
||||
|
||||
&-controls {
|
||||
@@ -20,15 +20,15 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { MaterialModule } from 'src/app/material.module';
|
||||
import { RegistrarService } from 'src/app/registrar/registrar.service';
|
||||
import { BackendService } from 'src/app/shared/services/backend.service';
|
||||
import WhoisComponent from './whois.component';
|
||||
import RdapComponent from './rdap.component';
|
||||
|
||||
describe('WhoisComponent', () => {
|
||||
let component: WhoisComponent;
|
||||
let fixture: ComponentFixture<WhoisComponent>;
|
||||
describe('RdapComponent', () => {
|
||||
let component: RdapComponent;
|
||||
let fixture: ComponentFixture<RdapComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [WhoisComponent],
|
||||
declarations: [RdapComponent],
|
||||
imports: [MaterialModule, BrowserAnimationsModule],
|
||||
providers: [
|
||||
BackendService,
|
||||
@@ -45,7 +45,7 @@ describe('WhoisComponent', () => {
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(WhoisComponent);
|
||||
fixture = TestBed.createComponent(RdapComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
@@ -14,16 +14,16 @@
|
||||
|
||||
import { Component, computed } from '@angular/core';
|
||||
import { RegistrarService } from 'src/app/registrar/registrar.service';
|
||||
|
||||
import { WhoisService } from './whois.service';
|
||||
import { RdapService } from './rdap.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-whois',
|
||||
templateUrl: './whois.component.html',
|
||||
styleUrls: ['./whois.component.scss'],
|
||||
selector: 'app-rdap',
|
||||
templateUrl: './rdap.component.html',
|
||||
styleUrls: ['./rdap.component.scss'],
|
||||
standalone: false,
|
||||
})
|
||||
export default class WhoisComponent {
|
||||
public static PATH = 'whois';
|
||||
export default class RdapComponent {
|
||||
public static PATH = 'rdap';
|
||||
formattedAddress = computed(() => {
|
||||
let result = '';
|
||||
const registrar = this.registrarService.registrar();
|
||||
@@ -46,7 +46,7 @@ export default class WhoisComponent {
|
||||
});
|
||||
|
||||
constructor(
|
||||
public whoisService: WhoisService,
|
||||
public rdapService: RdapService,
|
||||
public registrarService: RegistrarService
|
||||
) {}
|
||||
}
|
||||
@@ -16,14 +16,14 @@ import { Injectable } from '@angular/core';
|
||||
import { switchMap } from 'rxjs';
|
||||
import {
|
||||
RegistrarService,
|
||||
WhoisRegistrarFields,
|
||||
RdapRegistrarFields,
|
||||
} from 'src/app/registrar/registrar.service';
|
||||
import { BackendService } from 'src/app/shared/services/backend.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class WhoisService {
|
||||
export class RdapService {
|
||||
editing: boolean = false;
|
||||
|
||||
constructor(
|
||||
@@ -31,8 +31,8 @@ export class WhoisService {
|
||||
private registrarService: RegistrarService
|
||||
) {}
|
||||
|
||||
saveChanges(newWhoisRegistrarFields: WhoisRegistrarFields) {
|
||||
return this.backend.postWhoisRegistrarFields(newWhoisRegistrarFields).pipe(
|
||||
saveChanges(newRdapRegistrarFields: RdapRegistrarFields) {
|
||||
return this.backend.postRdapRegistrarFields(newRdapRegistrarFields).pipe(
|
||||
switchMap(() => {
|
||||
return this.registrarService.loadRegistrars();
|
||||
})
|
||||
@@ -1,22 +1,27 @@
|
||||
<div class="console-app__whois-edit" *ngIf="registrarInEdit">
|
||||
<div
|
||||
class="console-app__rdap-edit"
|
||||
*ngIf="registrarInEdit"
|
||||
cdkTrapFocus
|
||||
[cdkTrapFocusAutoCapture]="true"
|
||||
>
|
||||
<button
|
||||
mat-icon-button
|
||||
class="console-app__whois-edit-back"
|
||||
aria-label="Back to whois view"
|
||||
(click)="whoisService.editing = false"
|
||||
class="console-app__rdap-edit-back"
|
||||
aria-label="Back to rdap view"
|
||||
(click)="rdapService.editing = false"
|
||||
>
|
||||
<mat-icon>arrow_back</mat-icon>
|
||||
</button>
|
||||
|
||||
<div class="console-app__whois-edit-controls">
|
||||
<div class="console-app__rdap-edit-controls">
|
||||
<span>
|
||||
General registrar information for your WHOIS record. This information is
|
||||
always visible in WHOIS.
|
||||
General registrar information for your RDAP record. This information is
|
||||
always visible in RDAP.
|
||||
</span>
|
||||
<div class="spacer"></div>
|
||||
</div>
|
||||
|
||||
<div class="console-app__whois-edit">
|
||||
<div class="console-app__rdap-edit">
|
||||
<h1>Personal info</h1>
|
||||
|
||||
<form (ngSubmit)="save($event)">
|
||||
@@ -110,41 +115,14 @@
|
||||
/>
|
||||
</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>
|
||||
|
||||
@if((userDataService.userData()?.globalRole || 'NONE') !== "NONE") {
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>ICANN Referral Email: </mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="text"
|
||||
[(ngModel)]="registrarInEdit.icannReferralEmail"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
/>
|
||||
</mat-form-field>
|
||||
}
|
||||
|
||||
<button mat-flat-button color="primary" type="submit">Save</button>
|
||||
<button
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
type="submit"
|
||||
aria-label="Save RDAO settings"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,4 +1,4 @@
|
||||
.console-app__whois-edit {
|
||||
.console-app__rdap-edit {
|
||||
max-width: 616px;
|
||||
|
||||
&-controls {
|
||||
@@ -20,19 +20,20 @@ import {
|
||||
RegistrarService,
|
||||
} from 'src/app/registrar/registrar.service';
|
||||
import { UserDataService } from 'src/app/shared/services/userData.service';
|
||||
import { WhoisService } from './whois.service';
|
||||
import { RdapService } from './rdap.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-whois-edit',
|
||||
templateUrl: './whoisEdit.component.html',
|
||||
styleUrls: ['./whoisEdit.component.scss'],
|
||||
selector: 'app-rdap-edit',
|
||||
templateUrl: './rdapEdit.component.html',
|
||||
styleUrls: ['./rdapEdit.component.scss'],
|
||||
standalone: false,
|
||||
})
|
||||
export default class WhoisEditComponent {
|
||||
export default class RdapEditComponent {
|
||||
registrarInEdit: Registrar | undefined;
|
||||
|
||||
constructor(
|
||||
public userDataService: UserDataService,
|
||||
public whoisService: WhoisService,
|
||||
public rdapService: RdapService,
|
||||
public registrarService: RegistrarService,
|
||||
private _snackBar: MatSnackBar
|
||||
) {
|
||||
@@ -48,9 +49,9 @@ export default class WhoisEditComponent {
|
||||
e.preventDefault();
|
||||
if (!this.registrarInEdit) return;
|
||||
|
||||
this.whoisService.saveChanges(this.registrarInEdit).subscribe({
|
||||
this.rdapService.saveChanges(this.registrarInEdit).subscribe({
|
||||
complete: () => {
|
||||
this.whoisService.editing = false;
|
||||
this.rdapService.editing = false;
|
||||
},
|
||||
error: (err: HttpErrorResponse) => {
|
||||
this._snackBar.open(err.error);
|
||||
@@ -1,4 +1,8 @@
|
||||
<div class="settings-security__edit-password">
|
||||
<div
|
||||
class="settings-security__edit-password"
|
||||
cdkTrapFocus
|
||||
[cdkTrapFocusAutoCapture]="true"
|
||||
>
|
||||
<p>
|
||||
<button
|
||||
mat-icon-button
|
||||
@@ -12,65 +16,22 @@
|
||||
<p class="secondary-text">
|
||||
Passwords must be between 6 and 16 alphanumeric characters
|
||||
</p>
|
||||
<form
|
||||
(ngSubmit)="save()"
|
||||
<password-input-form-component
|
||||
[displayOldPasswordField]="true"
|
||||
[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>
|
||||
(submitResults)="save($event)"
|
||||
/>
|
||||
@if(userDataService.userData()?.isAdmin) {
|
||||
<div class="settings-security__reset-password-field">
|
||||
<h2>Need to reset your EPP password?</h2>
|
||||
<button
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
[disabled]="!passwordUpdateForm.valid"
|
||||
aria-label="Save epp password update"
|
||||
type="submit"
|
||||
class="settings-security__edit-password-save"
|
||||
aria-label="Reset EPP password via email"
|
||||
(click)="requestEppPasswordReset()"
|
||||
>
|
||||
Save
|
||||
Reset EPP password via email
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
.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;
|
||||
// Copyright 2025 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
.settings-security {
|
||||
&__reset-password-field {
|
||||
margin-top: 60px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,97 +14,86 @@
|
||||
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { Component } from '@angular/core';
|
||||
import {
|
||||
AbstractControl,
|
||||
FormControl,
|
||||
FormGroup,
|
||||
ValidatorFn,
|
||||
Validators,
|
||||
} from '@angular/forms';
|
||||
import { FormControl, FormGroup, Validators } from '@angular/forms';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { RegistrarService } from 'src/app/registrar/registrar.service';
|
||||
import { SecurityService } from './security.service';
|
||||
import { UserDataService } from 'src/app/shared/services/userData.service';
|
||||
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { MaterialModule } from 'src/app/material.module';
|
||||
import { filter, switchMap, take } from 'rxjs';
|
||||
import { BackendService } from 'src/app/shared/services/backend.service';
|
||||
import {
|
||||
PasswordInputForm,
|
||||
PasswordResults,
|
||||
} from 'src/app/shared/components/passwordReset/passwordInputForm.component';
|
||||
|
||||
type errorCode = 'required' | 'maxlength' | 'minlength' | 'passwordsDontMatch';
|
||||
@Component({
|
||||
selector: 'app-reset-epp-password-dialog',
|
||||
template: `
|
||||
<h2 mat-dialog-title>Please confirm the password reset:</h2>
|
||||
<mat-dialog-content>
|
||||
This will send an EPP password reset email to the admin POC.
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions>
|
||||
<button mat-button (click)="onCancel()">Cancel</button>
|
||||
<button mat-button color="warn" (click)="onSave()">Confirm</button>
|
||||
</mat-dialog-actions>
|
||||
`,
|
||||
imports: [CommonModule, MaterialModule],
|
||||
})
|
||||
export class ResetEppPasswordComponent {
|
||||
constructor(public dialogRef: MatDialogRef<ResetEppPasswordComponent>) {}
|
||||
|
||||
type errorFriendlyText = { [type in errorCode]: String };
|
||||
onSave(): void {
|
||||
this.dialogRef.close(true);
|
||||
}
|
||||
|
||||
onCancel(): void {
|
||||
this.dialogRef.close(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-epp-password-edit',
|
||||
templateUrl: './eppPasswordEdit.component.html',
|
||||
styleUrls: ['./eppPasswordEdit.component.scss'],
|
||||
standalone: false,
|
||||
})
|
||||
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;
|
||||
};
|
||||
static EPP_VALIDATORS = [
|
||||
Validators.required,
|
||||
Validators.minLength(6),
|
||||
Validators.maxLength(16),
|
||||
PasswordInputForm.newPasswordsMatch,
|
||||
];
|
||||
|
||||
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,
|
||||
]),
|
||||
newPassword: new FormControl('', EppPasswordEditComponent.EPP_VALIDATORS),
|
||||
newPasswordRepeat: new FormControl(
|
||||
'',
|
||||
EppPasswordEditComponent.EPP_VALIDATORS
|
||||
),
|
||||
});
|
||||
|
||||
save() {
|
||||
const { oldPassword, newPassword, newPasswordRepeat } =
|
||||
this.passwordUpdateForm.value;
|
||||
if (!oldPassword || !newPassword || !newPasswordRepeat) return;
|
||||
constructor(
|
||||
public registrarService: RegistrarService,
|
||||
public securityService: SecurityService,
|
||||
protected userDataService: UserDataService,
|
||||
private backendService: BackendService,
|
||||
private resetPasswordDialog: MatDialog,
|
||||
private _snackBar: MatSnackBar
|
||||
) {}
|
||||
|
||||
save(passwordResults: PasswordResults) {
|
||||
this.securityService
|
||||
.saveEppPassword({
|
||||
registrarId: this.registrarService.registrarId(),
|
||||
oldPassword,
|
||||
newPassword,
|
||||
newPasswordRepeat,
|
||||
oldPassword: passwordResults.oldPassword!,
|
||||
newPassword: passwordResults.newPassword,
|
||||
newPasswordRepeat: passwordResults.newPasswordRepeat,
|
||||
})
|
||||
.subscribe({
|
||||
complete: () => {
|
||||
@@ -119,4 +108,26 @@ export default class EppPasswordEditComponent {
|
||||
goBack() {
|
||||
this.securityService.isEditingPassword = false;
|
||||
}
|
||||
|
||||
sendEppPasswordResetRequest() {
|
||||
return this.backendService.requestEppPasswordReset(
|
||||
this.registrarService.registrarId()
|
||||
);
|
||||
}
|
||||
|
||||
requestEppPasswordReset() {
|
||||
const dialogRef = this.resetPasswordDialog.open(ResetEppPasswordComponent);
|
||||
dialogRef
|
||||
.afterClosed()
|
||||
.pipe(
|
||||
take(1),
|
||||
filter((result) => !!result)
|
||||
)
|
||||
.pipe(switchMap((_) => this.sendEppPasswordResetRequest()))
|
||||
.subscribe({
|
||||
next: (_) => this.goBack(),
|
||||
error: (err: HttpErrorResponse) =>
|
||||
this._snackBar.open(err.error || err.message),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ describe('SecurityComponent', () => {
|
||||
fetchSecurityDetailsSpy =
|
||||
securityServiceSpy.fetchSecurityDetails.and.returnValue(of());
|
||||
|
||||
saveSpy = securityServiceSpy.saveChanges;
|
||||
saveSpy = securityServiceSpy.saveChanges.and.returnValue(of());
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [SecurityEditComponent, SecurityComponent],
|
||||
|
||||
@@ -23,6 +23,7 @@ import { SecurityService, apiToUiConverter } from './security.service';
|
||||
selector: 'app-security',
|
||||
templateUrl: './security.component.html',
|
||||
styleUrls: ['./security.component.scss'],
|
||||
standalone: false,
|
||||
})
|
||||
export default class SecurityComponent {
|
||||
public static PATH = 'security';
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { switchMap, timeout } from 'rxjs';
|
||||
import { switchMap } from 'rxjs';
|
||||
import {
|
||||
IpAllowListItem,
|
||||
RegistrarService,
|
||||
@@ -69,7 +69,6 @@ export class SecurityService {
|
||||
uiToApiConverter(newSecuritySettings)
|
||||
)
|
||||
.pipe(
|
||||
timeout(2000),
|
||||
switchMap(() => {
|
||||
return this.registrarService.loadRegistrars();
|
||||
})
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
<div class="settings-security__edit">
|
||||
<div
|
||||
class="settings-security__edit"
|
||||
cdkTrapFocus
|
||||
[cdkTrapFocusAutoCapture]="true"
|
||||
>
|
||||
<h1>IP Allowlist</h1>
|
||||
<p>
|
||||
Restrict access to EPP production servers to the following IP/IPv6
|
||||
@@ -19,7 +23,7 @@
|
||||
<button
|
||||
matSuffix
|
||||
mat-icon-button
|
||||
aria-label="Remove"
|
||||
[attr.aria-label]="'Remove IP entry ' + ip.value"
|
||||
(click)="removeIpEntry(ip)"
|
||||
[disabled]="isUpdating"
|
||||
>
|
||||
@@ -32,6 +36,7 @@
|
||||
[disabled]="isUpdating"
|
||||
color="primary"
|
||||
(click)="createIpEntry()"
|
||||
aria-label="Add new IP address"
|
||||
type="button"
|
||||
>
|
||||
+ Add IP
|
||||
|
||||
@@ -26,6 +26,7 @@ import { SecurityService, apiToUiConverter } from './security.service';
|
||||
selector: 'app-security-edit',
|
||||
templateUrl: './securityEdit.component.html',
|
||||
styleUrls: ['./securityEdit.component.scss'],
|
||||
standalone: false,
|
||||
})
|
||||
export default class SecurityEditComponent {
|
||||
dataSource: SecuritySettings = {};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<app-selected-registrar-wrapper>
|
||||
<div class="console-settings">
|
||||
<h1 class="mat-headline-4">Settings</h1>
|
||||
<h1 class="mat-headline-4" forceFocus>Settings</h1>
|
||||
<nav
|
||||
mat-tab-nav-bar
|
||||
mat-stretch-tabs="false"
|
||||
@@ -10,22 +10,31 @@
|
||||
<a
|
||||
mat-tab-link
|
||||
routerLink="contact"
|
||||
routerLinkActive="active-link"
|
||||
routerLinkActive
|
||||
queryParamsHandling="merge"
|
||||
#rla="routerLinkActive"
|
||||
[active]="rla.isActive"
|
||||
aria-label="Access contacts settings"
|
||||
>Contacts</a
|
||||
>
|
||||
<a
|
||||
mat-tab-link
|
||||
routerLink="whois"
|
||||
routerLinkActive="active-link"
|
||||
routerLink="rdap"
|
||||
routerLinkActive
|
||||
queryParamsHandling="merge"
|
||||
>WHOIS Info</a
|
||||
#rla2="routerLinkActive"
|
||||
[active]="rla2.isActive"
|
||||
aria-label="Access rdap settings"
|
||||
>RDAP Info</a
|
||||
>
|
||||
<a
|
||||
mat-tab-link
|
||||
routerLink="security"
|
||||
routerLinkActive="active-link"
|
||||
routerLinkActive
|
||||
queryParamsHandling="merge"
|
||||
#rla3="routerLinkActive"
|
||||
[active]="rla3.isActive"
|
||||
aria-label="Access security settings"
|
||||
>Security</a
|
||||
>
|
||||
</nav>
|
||||
|
||||
@@ -13,14 +13,6 @@
|
||||
// limitations under the License.
|
||||
|
||||
.console-settings {
|
||||
.mdc-tab {
|
||||
&.active-link {
|
||||
border-bottom: 2px solid var(--primary);
|
||||
.mdc-tab__text-label {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
nav {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,12 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { SettingsComponent } from './settings.component';
|
||||
import { MaterialModule } from '../material.module';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { AppModule, SelectedRegistrarModule } from '../app.module';
|
||||
import { BackendService } from '../shared/services/backend.service';
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
import { provideHttpClientTesting } from '@angular/common/http/testing';
|
||||
import { AppRoutingModule } from '../app-routing.module';
|
||||
|
||||
describe('SettingsComponent', () => {
|
||||
let component: SettingsComponent;
|
||||
@@ -24,7 +30,19 @@ describe('SettingsComponent', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [MaterialModule, BrowserAnimationsModule],
|
||||
imports: [
|
||||
SelectedRegistrarModule,
|
||||
MaterialModule,
|
||||
BrowserAnimationsModule,
|
||||
AppRoutingModule,
|
||||
AppModule,
|
||||
],
|
||||
providers: [
|
||||
BackendService,
|
||||
{ provide: ActivatedRoute, useValue: {} as ActivatedRoute },
|
||||
provideHttpClient(),
|
||||
provideHttpClientTesting(),
|
||||
],
|
||||
declarations: [SettingsComponent],
|
||||
}).compileComponents();
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import { Component, ViewEncapsulation } from '@angular/core';
|
||||
templateUrl: './settings.component.html',
|
||||
styleUrls: ['./settings.component.scss'],
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
standalone: false,
|
||||
})
|
||||
export class SettingsComponent {
|
||||
public static PATH = 'settings';
|
||||
|
||||
@@ -24,6 +24,7 @@ interface Notification {
|
||||
selector: 'app-notifications',
|
||||
templateUrl: './notifications.component.html',
|
||||
styleUrls: ['./notifications.component.scss'],
|
||||
standalone: false,
|
||||
})
|
||||
export class NotificationsComponent {
|
||||
protected mockNotifications: Notification[] = [
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
<form
|
||||
(ngSubmit)="save()"
|
||||
[formGroup]="formGroup()!"
|
||||
class="console-app__password-input-form"
|
||||
>
|
||||
@if (displayOldPasswordField()) {
|
||||
<div class="console-app__password-input-form-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="console-app__password-input-form-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="console-app__password-input-form-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]="!formGroup()?.valid"
|
||||
aria-label="Save new password"
|
||||
type="submit"
|
||||
class="console-app__password-input-form-save"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</form>
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||
// Copyright 2025 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -12,17 +12,19 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
.console-tlds {
|
||||
&__cards {
|
||||
display: flex;
|
||||
border-top: 1px solid #ddd;
|
||||
padding: 1rem;
|
||||
.console-app__password-input-form {
|
||||
max-width: 450px;
|
||||
&-field {
|
||||
width: 100%;
|
||||
mat-form-field {
|
||||
margin-bottom: 20px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
&__card {
|
||||
max-width: 300px;
|
||||
&-form {
|
||||
margin-top: 30px;
|
||||
}
|
||||
&__card-links {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
&-save {
|
||||
margin-top: 30px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
// Copyright 2025 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, EventEmitter, input, Output } from '@angular/core';
|
||||
import { AbstractControl, FormGroup, ValidatorFn } from '@angular/forms';
|
||||
|
||||
type errorCode = 'required' | 'maxlength' | 'minlength' | 'passwordsDontMatch';
|
||||
|
||||
type errorFriendlyText = { [type in errorCode]: String };
|
||||
|
||||
export interface PasswordResults {
|
||||
oldPassword: string | null;
|
||||
newPassword: string;
|
||||
newPasswordRepeat: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'password-input-form-component',
|
||||
templateUrl: './passwordInputForm.component.html',
|
||||
styleUrls: ['./passwordInputForm.component.scss'],
|
||||
standalone: false,
|
||||
})
|
||||
export class PasswordInputForm {
|
||||
static newPasswordsMatch: ValidatorFn = (control: AbstractControl) => {
|
||||
const parent = control.parent;
|
||||
if (
|
||||
parent?.get('newPassword')?.value ===
|
||||
parent?.get('newPasswordRepeat')?.value
|
||||
) {
|
||||
parent?.get('newPasswordRepeat')?.setErrors(null);
|
||||
} else {
|
||||
// latest angular just won't detect the error without setTimeout
|
||||
setTimeout(() => {
|
||||
parent
|
||||
?.get('newPasswordRepeat')
|
||||
?.setErrors({ passwordsDontMatch: control.value });
|
||||
});
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
MIN_MAX_LENGTH = 'Passwords must be between 6 and 16 alphanumeric characters';
|
||||
|
||||
errorTextMap: errorFriendlyText = {
|
||||
required: "This field can't be empty",
|
||||
maxlength: this.MIN_MAX_LENGTH,
|
||||
minlength: this.MIN_MAX_LENGTH,
|
||||
passwordsDontMatch: "Passwords don't match",
|
||||
};
|
||||
|
||||
displayOldPasswordField = input<boolean>(false);
|
||||
formGroup = input<FormGroup>();
|
||||
@Output() submitResults = new EventEmitter<PasswordResults>();
|
||||
|
||||
hasError(controlName: string) {
|
||||
const maybeErrors = this.formGroup()!.get(controlName)?.errors;
|
||||
const maybeError =
|
||||
maybeErrors && (Object.keys(maybeErrors)[0] as errorCode);
|
||||
if (maybeError) {
|
||||
return this.errorTextMap[maybeError];
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
save() {
|
||||
const results: PasswordResults = this.formGroup()!.value;
|
||||
if (this.displayOldPasswordField() && !results.oldPassword) return;
|
||||
if (!results.newPassword || !results.newPasswordRepeat) return;
|
||||
this.submitResults.emit(results);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<p>
|
||||
<button mat-icon-button aria-label="Go home" [routerLink]="['']">
|
||||
<mat-icon>arrow_back</mat-icon>
|
||||
</button>
|
||||
</p>
|
||||
@if (isLoading) {
|
||||
<div class="console-app__password-reset-verify-spinner">
|
||||
<mat-spinner />
|
||||
</div>
|
||||
} @else if (errorMessage) {
|
||||
<h1 class="mat-headline-4">Failure</h1>
|
||||
<div class="console-app__password-reset-content">
|
||||
<div class="console-app__password-reset-subhead">
|
||||
An error occurred: {{ errorMessage }}.<br /><br />Please double-check the
|
||||
verification code and try again.
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="console-app__password-reset-verify">
|
||||
<h1 class="mat-headline-4">{{ type }} password reset</h1>
|
||||
<password-input-form-component
|
||||
[displayOldPasswordField]="false"
|
||||
[formGroup]="passwordUpdateForm!"
|
||||
(submitResults)="save($event)"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
// Copyright 2025 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component } from '@angular/core';
|
||||
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { take } from 'rxjs';
|
||||
import { RegistrarService } from 'src/app/registrar/registrar.service';
|
||||
import { BackendService } from '../../services/backend.service';
|
||||
import { FormControl, FormGroup, Validators } from '@angular/forms';
|
||||
import {
|
||||
PasswordInputForm,
|
||||
PasswordResults,
|
||||
} from './passwordInputForm.component';
|
||||
import EppPasswordEditComponent from 'src/app/settings/security/eppPasswordEdit.component';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
|
||||
export interface PasswordResetVerifyResponse {
|
||||
registrarId: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-password-reset-verify',
|
||||
templateUrl: './passwordResetVerify.component.html',
|
||||
standalone: false,
|
||||
})
|
||||
export class PasswordResetVerifyComponent {
|
||||
public static PATH = 'password-reset-verify';
|
||||
|
||||
REGISTRY_LOCK_PASSWORD_VALIDATORS = [
|
||||
Validators.required,
|
||||
PasswordInputForm.newPasswordsMatch,
|
||||
];
|
||||
|
||||
isLoading = true;
|
||||
type?: string;
|
||||
errorMessage?: string;
|
||||
requestVerificationCode = '';
|
||||
|
||||
passwordUpdateForm: FormGroup<any> | null = null;
|
||||
|
||||
constructor(
|
||||
protected backendService: BackendService,
|
||||
protected registrarService: RegistrarService,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private _snackBar: MatSnackBar
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.route.queryParamMap.pipe(take(1)).subscribe((params: ParamMap) => {
|
||||
this.requestVerificationCode =
|
||||
params.get('resetRequestVerificationCode') || '';
|
||||
this.backendService
|
||||
.getPasswordResetInformation(this.requestVerificationCode)
|
||||
.subscribe({
|
||||
error: (err: HttpErrorResponse) => {
|
||||
this.isLoading = false;
|
||||
this.errorMessage = err.error;
|
||||
},
|
||||
next: this.presentData.bind(this),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
presentData(verificationResponse: PasswordResetVerifyResponse) {
|
||||
this.type = verificationResponse.type === 'EPP' ? 'EPP' : 'Registry lock';
|
||||
this.registrarService.registrarId.set(verificationResponse.registrarId);
|
||||
const validators =
|
||||
verificationResponse.type === 'EPP'
|
||||
? EppPasswordEditComponent.EPP_VALIDATORS
|
||||
: this.REGISTRY_LOCK_PASSWORD_VALIDATORS;
|
||||
|
||||
this.passwordUpdateForm = new FormGroup({
|
||||
newPassword: new FormControl('', validators),
|
||||
newPasswordRepeat: new FormControl('', validators),
|
||||
});
|
||||
this.isLoading = false;
|
||||
}
|
||||
|
||||
save(passwordResults: PasswordResults) {
|
||||
this.backendService
|
||||
.finalizePasswordReset(
|
||||
this.requestVerificationCode,
|
||||
passwordResults.newPassword
|
||||
)
|
||||
.subscribe({
|
||||
error: (err: HttpErrorResponse) => {
|
||||
this.isLoading = false;
|
||||
this.errorMessage = err.error;
|
||||
},
|
||||
next: (_) => {
|
||||
this.router.navigate(['']);
|
||||
this._snackBar.open('Password reset completed successfully');
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<div class="console-app__pocReminder">
|
||||
<p class="">
|
||||
Please take a moment to complete annual review of
|
||||
<a routerLink="/settings">contacts</a>.
|
||||
</p>
|
||||
<span matSnackBarActions>
|
||||
<button mat-button matSnackBarAction (click)="confirmReviewed()">
|
||||
Confirm reviewed
|
||||
</button>
|
||||
<button mat-button matSnackBarAction (click)="snackBarRef.dismiss()">
|
||||
Close
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
@@ -0,0 +1,5 @@
|
||||
.console-app__pocReminder {
|
||||
a {
|
||||
color: white !important;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user