mirror of
https://github.com/google/nomulus
synced 2026-01-16 02:33:16 +00:00
Compare commits
450 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 | ||
|
|
35f95bbbe4 | ||
|
|
ae61cd443d | ||
|
|
cc20f7d76d | ||
|
|
5603b91526 | ||
|
|
332f491ac7 | ||
|
|
4bd7c18fe9 | ||
|
|
fdb0664841 | ||
|
|
a9ba770bfa | ||
|
|
4d96e5a6b1 | ||
|
|
1171c5cfcb | ||
|
|
91e241374d | ||
|
|
634202c0e9 | ||
|
|
020ed33003 | ||
|
|
0f61066b1d | ||
|
|
03711481cd | ||
|
|
c32fb2fc71 | ||
|
|
6e77c89cd6 | ||
|
|
5e41e84b8d | ||
|
|
bfd569ee44 | ||
|
|
b13a33347f | ||
|
|
d17a6edf12 | ||
|
|
7255ebff29 | ||
|
|
cacc90097a | ||
|
|
0ef8984767 | ||
|
|
7a4abd93dc | ||
|
|
142c910e3b | ||
|
|
c68d54a5ed | ||
|
|
d17188b820 | ||
|
|
cbe59b6950 | ||
|
|
2b3c6525ff | ||
|
|
72dd8658cf | ||
|
|
c0490f7777 | ||
|
|
a22a38527b | ||
|
|
08203033a2 | ||
|
|
d0482a8f2c | ||
|
|
e6a2db8075 | ||
|
|
7929322e95 | ||
|
|
5c35811eb9 | ||
|
|
4ba0f4a2cd | ||
|
|
e167b4b753 | ||
|
|
c47f821754 | ||
|
|
febdbc0468 | ||
|
|
a988732d65 | ||
|
|
7ee541e1b1 | ||
|
|
b07769bdee | ||
|
|
9db016638e | ||
|
|
c3d164d462 | ||
|
|
352618b3b7 | ||
|
|
0389b0d2d9 | ||
|
|
8906a82e3b | ||
|
|
f6e42896c3 | ||
|
|
4d3dec54cf | ||
|
|
f082ffffe3 | ||
|
|
5f23f2a15a | ||
|
|
7ed7cf3340 | ||
|
|
ab60ac44fd | ||
|
|
d9ad39cdad | ||
|
|
bac4e22bff | ||
|
|
ab5f6cc229 | ||
|
|
1765f4f0b4 | ||
|
|
e88c6e1550 | ||
|
|
1739c6d74f | ||
|
|
66513a114e | ||
|
|
0e808a4c01 | ||
|
|
4e013603be | ||
|
|
730585cd14 | ||
|
|
fd7820759d | ||
|
|
69359bb1e6 | ||
|
|
35b602a76e | ||
|
|
82002d1f75 | ||
|
|
2fd9b062df | ||
|
|
ec3804e87e | ||
|
|
d0d28cc7e6 | ||
|
|
2d1260c01b | ||
|
|
06da6a2cc6 | ||
|
|
858a22f82e | ||
|
|
3c126ddfd4 | ||
|
|
2b98e6f177 | ||
|
|
20036b6a74 | ||
|
|
396cbd6bd3 | ||
|
|
71ea16ff69 | ||
|
|
45331be166 | ||
|
|
beb7c14adb | ||
|
|
d33571dde3 | ||
|
|
53a7d1b66c | ||
|
|
fa721e82ff | ||
|
|
d4faa77ee4 | ||
|
|
96d3d88c2f | ||
|
|
213e06f02e | ||
|
|
d5445dd049 | ||
|
|
af5adcb0ba | ||
|
|
ca238a8578 | ||
|
|
1a8f133d54 | ||
|
|
233ee09efe | ||
|
|
35ff768176 | ||
|
|
c4e5bc913e | ||
|
|
0241937dee | ||
|
|
68b46735cd | ||
|
|
bfeaf4a23e | ||
|
|
5f9f157494 | ||
|
|
c23eed6ec4 | ||
|
|
04a4431d6a | ||
|
|
d9c5d71f40 | ||
|
|
75f09c2fdf | ||
|
|
74f0a8dd7b | ||
|
|
092e3dca47 | ||
|
|
b8a6ac72dd | ||
|
|
b602aac09a | ||
|
|
d86c002132 | ||
|
|
54c5a9450d | ||
|
|
0f0097c15c | ||
|
|
c9437d8c72 | ||
|
|
19819444af | ||
|
|
15df3aea44 | ||
|
|
d000a5dff8 | ||
|
|
34694b4aef | ||
|
|
7ce7b23450 | ||
|
|
a5d1469281 | ||
|
|
a90a85afae | ||
|
|
6e68876a14 | ||
|
|
11231703d5 | ||
|
|
b77a219e19 | ||
|
|
bd8e6354b5 | ||
|
|
361094f537 | ||
|
|
d53177e44c | ||
|
|
e73f646e1f | ||
|
|
1a5dfb0ac2 | ||
|
|
49cb1875d1 | ||
|
|
61eee45ad0 | ||
|
|
e99a18f54f | ||
|
|
0c123e1676 | ||
|
|
81b239c6b3 | ||
|
|
ea8c34bf8b | ||
|
|
b3e67e58b5 | ||
|
|
589041b3ed | ||
|
|
455364ff29 | ||
|
|
d90bc1a3e4 | ||
|
|
0e3875c1ff | ||
|
|
3b565b96b7 | ||
|
|
ec6c77927f | ||
|
|
e88ff77ecb | ||
|
|
0781010b16 | ||
|
|
ab4bac05d1 | ||
|
|
8e22ce7c70 | ||
|
|
d96a5547ce | ||
|
|
05b43965d1 | ||
|
|
43000a5f80 | ||
|
|
a66b9ea749 | ||
|
|
36a660a8ad | ||
|
|
d09bb4ff74 | ||
|
|
6ca3cc230f | ||
|
|
6a5d8ed3b5 | ||
|
|
53dcba1189 | ||
|
|
3e77004274 | ||
|
|
fd21fcdb84 | ||
|
|
ae14e35df7 | ||
|
|
94dc9fd0d5 | ||
|
|
7b34659a8f | ||
|
|
808432e709 | ||
|
|
73d3b76a89 | ||
|
|
ca072b4861 | ||
|
|
54c896cbb9 | ||
|
|
2c7bf2cfdb |
10
.github/workflows/codeql.yml
vendored
10
.github/workflows/codeql.yml
vendored
@@ -6,8 +6,6 @@ on:
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ 'master' ]
|
||||
schedule:
|
||||
- cron: '24 4 * * *'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
@@ -49,13 +47,13 @@ jobs:
|
||||
|
||||
# Build with Gradle
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v3
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
with:
|
||||
build-scan-publish: true
|
||||
build-scan-terms-of-service-url: "https://gradle.com/terms-of-service"
|
||||
build-scan-terms-of-service-agree: "yes"
|
||||
build-scan-terms-of-use-url: "https://gradle.com/terms-of-service"
|
||||
build-scan-terms-of-use-agree: "yes"
|
||||
- name: Execute Gradle build
|
||||
run: ./gradlew build -x test -x jIFC
|
||||
run: ./gradlew --no-daemon --no-build-cache --no-configuration-cache --rerun-tasks clean build -x test -x jIFC
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
|
||||
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
|
||||
|
||||
@@ -34,6 +34,8 @@ Guy Bensky <guyben@google.com>
|
||||
Weimin Yu <weiminyu@google.com>
|
||||
Shicong Huang <shicong@google.com>
|
||||
Gustav Brodman <gbrodman@google.com>
|
||||
Aman Sanger <sangera@google.com>
|
||||
Sarah Botwinick <sarahbot@google.com>
|
||||
Legina Chen <legina@google.com>
|
||||
Rachel Guan <rachelguan@google.com>
|
||||
Juan Celhay <jicelhay@google.com>
|
||||
|
||||
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,124 +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("${rootDir}/console-webapp/dist/console-webapp") {
|
||||
include "**/*"
|
||||
into("console")
|
||||
}
|
||||
from("${coreResourcesDir}/google/registry/ui") {
|
||||
include "registrar_bin.js"
|
||||
if (environment != "production") {
|
||||
include "registrar_bin.js.map"
|
||||
}
|
||||
into("assets/js")
|
||||
}
|
||||
from("${coreResourcesDir}/google/registry/ui/css") {
|
||||
include "registrar*"
|
||||
into("assets/css")
|
||||
}
|
||||
from("${coreResourcesDir}/google/registry/ui/assets/images") {
|
||||
include "**/*"
|
||||
into("assets/images")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.finalizedBy ':deployCloudSchedulerAndQueue'
|
||||
rootProject.deploy.dependsOn appengineDeployAll
|
||||
rootProject.stage.dependsOn appengineStage
|
||||
tasks['war'].dependsOn ':console-webapp:buildConsoleWebappProd'
|
||||
tasks['war'].dependsOn ':core:compileProdJS'
|
||||
tasks['war'].dependsOn ':core:processResources'
|
||||
tasks['war'].dependsOn ':core:jar'
|
||||
|
||||
// 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
|
||||
42
build.gradle
42
build.gradle
@@ -60,7 +60,7 @@ dependencyLocking {
|
||||
|
||||
node {
|
||||
download = false
|
||||
version = "20.10.0"
|
||||
version = "22.7.0"
|
||||
}
|
||||
|
||||
wrapper {
|
||||
@@ -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,30 +95,27 @@ 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
|
||||
rootProject.ext.gcpProject = gcpProject
|
||||
rootProject.ext.baseDomain = baseDomains[environment]
|
||||
rootProject.ext.prodOrSandboxEnv = environment in ['production', 'sandbox']
|
||||
|
||||
// Function to verify that the deployment parameters have been set.
|
||||
@@ -274,6 +271,11 @@ subprojects {
|
||||
attributes 'Main-Class': mainClass
|
||||
}
|
||||
}
|
||||
// Build as a multi-release jar since we've got member jars (e.g., dnsjava
|
||||
// and snakeyaml) that are multi-release.
|
||||
manifest {
|
||||
attributes 'Multi-Release': true
|
||||
}
|
||||
zip64 = true
|
||||
archiveClassifier = ''
|
||||
archiveVersion = ''
|
||||
@@ -329,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
|
||||
}
|
||||
@@ -378,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.
|
||||
//
|
||||
@@ -539,8 +527,6 @@ task coreDev {
|
||||
dependsOn 'javadoc'
|
||||
dependsOn 'checkDependenciesDotGradle'
|
||||
dependsOn 'checkLicense'
|
||||
// TODO: @ptkach reenable after console design merged
|
||||
// dependsOn ':console-webapp:runConsoleWebappUnitTests'
|
||||
dependsOn ':core:check'
|
||||
dependsOn 'assemble'
|
||||
}
|
||||
@@ -557,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 {
|
||||
@@ -565,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,66 +3,68 @@
|
||||
# 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.10.4=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
|
||||
com.google.auto.value:auto-value-annotations:1.11.0=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
|
||||
com.google.auto.value:auto-value-annotations:1.9=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
|
||||
com.google.auto:auto-common:1.2.1=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
|
||||
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.26.1=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
|
||||
com.google.guava:guava:31.0.1-jre=checkstyle
|
||||
com.google.guava:guava:32.1.1-jre=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
|
||||
com.google.guava:guava:33.1.0-jre=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
|
||||
com.google.guava:guava:33.2.1-android=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
|
||||
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=checkstyle,compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
|
||||
com.google.inject:guice:5.1.0=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
|
||||
com.google.j2objc:j2objc-annotations:1.3=checkstyle
|
||||
com.google.j2objc:j2objc-annotations:3.0.0=compileClasspath,testCompileClasspath,testingCompileClasspath
|
||||
com.google.protobuf:protobuf-java:3.19.6=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
|
||||
com.google.truth:truth:1.4.2=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
|
||||
com.google.truth:truth:1.4.4=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
|
||||
com.puppycrawl.tools:checkstyle:9.3=checkstyle
|
||||
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.12.7=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
|
||||
org.hamcrest:hamcrest-core:1.3=testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
|
||||
org.jacoco:org.jacoco.agent:0.8.11=jacocoAgent,jacocoAnt
|
||||
org.jacoco:org.jacoco.ant:0.8.11=jacocoAnt
|
||||
org.jacoco:org.jacoco.core:0.8.11=jacocoAnt
|
||||
org.jacoco:org.jacoco.report:0.8.11=jacocoAnt
|
||||
org.jacoco:org.jacoco.agent:0.8.12=jacocoAgent,jacocoAnt
|
||||
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.junit.jupiter:junit-jupiter-api:5.10.2=testCompileClasspath,testRuntimeClasspath
|
||||
org.junit.jupiter:junit-jupiter-engine:5.10.2=testCompileClasspath,testRuntimeClasspath
|
||||
org.junit.platform:junit-platform-commons:1.10.2=testCompileClasspath,testRuntimeClasspath
|
||||
org.junit.platform:junit-platform-engine:1.10.2=testCompileClasspath,testRuntimeClasspath
|
||||
org.junit:junit-bom:5.10.2=testCompileClasspath,testRuntimeClasspath
|
||||
org.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.6=jacocoAnt
|
||||
org.ow2.asm:asm-tree:9.6=jacocoAnt
|
||||
org.ow2.asm:asm:9.6=compileClasspath,deploy_jar,jacocoAnt,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
|
||||
org.ow2.asm:asm-commons:9.7=jacocoAnt
|
||||
org.ow2.asm:asm-tree:9.7=jacocoAnt
|
||||
org.ow2.asm:asm:9.7=compileClasspath,deploy_jar,jacocoAnt,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
|
||||
org.pcollections:pcollections:3.1.4=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
|
||||
org.reflections:reflections:0.10.2=checkstyle
|
||||
empty=testingCompile,testingRuntime,testingRuntimeClasspath
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -27,6 +27,9 @@
|
||||
{
|
||||
"moduleLicense": "Apache License V2.0"
|
||||
},
|
||||
{
|
||||
"moduleLicense": "Apache License Version 2.0"
|
||||
},
|
||||
{
|
||||
"moduleLicense": "Apache License, Version 2.0"
|
||||
},
|
||||
@@ -102,6 +105,12 @@
|
||||
{
|
||||
"moduleLicense": "BSD-2-Clause"
|
||||
},
|
||||
{
|
||||
"moduleLicense": "BSD 3-Clause \"New\" or \"Revised\" License (BSD-3-Clause)"
|
||||
},
|
||||
{
|
||||
"moduleLicense": "BSD licence"
|
||||
},
|
||||
{
|
||||
"moduleLicense": "New BSD License"
|
||||
},
|
||||
@@ -141,6 +150,9 @@
|
||||
{
|
||||
"moduleLicense": "\\n Dual license consisting of the CDDL v1.1 and GPL v2\\n "
|
||||
},
|
||||
{
|
||||
"moduleLicense": "EPL-2.0"
|
||||
},
|
||||
{
|
||||
"moduleLicense": "Eclipse Distribution License (New BSD License)"
|
||||
},
|
||||
@@ -192,6 +204,9 @@
|
||||
{
|
||||
"moduleLicense": "GNU GENERAL PUBLIC LICENSE, Version 2 + Classpath Exception"
|
||||
},
|
||||
{
|
||||
"moduleLicense": "(GPL-2.0-only WITH Classpath-exception-2.0)"
|
||||
},
|
||||
{
|
||||
"moduleLicense": "GNU Library General Public License v2.1 or later"
|
||||
},
|
||||
@@ -262,6 +277,12 @@
|
||||
{
|
||||
"moduleLicense": "Unicode/ICU License"
|
||||
},
|
||||
{
|
||||
"moduleLicense": "Unicode-3.0"
|
||||
},
|
||||
{
|
||||
"moduleLicense": "The W3C Software License"
|
||||
},
|
||||
{
|
||||
"moduleLicense": "Public Domain",
|
||||
"moduleName": "aopalliance:aopalliance"
|
||||
@@ -274,12 +295,6 @@
|
||||
"moduleLicense": "Public Domain",
|
||||
"moduleName": "org.json:json"
|
||||
},
|
||||
{
|
||||
// "Apache License, Version 2.0".
|
||||
"moduleLicense": null,
|
||||
"moduleVersion": "2.10.0",
|
||||
"moduleName": "com.google.gwt:gwt-user"
|
||||
},
|
||||
{
|
||||
// "Apache License, Version 2.0". The plugin is able to parse up to
|
||||
// 2.11.3 correctly but then something changed with 2.12.* and it no
|
||||
@@ -287,6 +302,11 @@
|
||||
"moduleLicense": null,
|
||||
"moduleName": "com.fasterxml.jackson:jackson-bom"
|
||||
},
|
||||
{
|
||||
// "Apache License, Version 2.0".
|
||||
"moduleLicense": null,
|
||||
"moduleName": "com.google.cloud:libraries-bom"
|
||||
},
|
||||
{
|
||||
// Part of Guava with "Apache License, Version 2.0". The plugin is unable
|
||||
// to parse its license for unknown reason.
|
||||
@@ -294,38 +314,27 @@
|
||||
"moduleName": "com.google.guava:guava-parent"
|
||||
},
|
||||
{
|
||||
// "Apache License, Version 2.0". The plugin is able to parse up to
|
||||
// 2.0.33.Final but not this verson.
|
||||
// "Apache License, Version 2.0".
|
||||
"moduleLicense": null,
|
||||
"moduleVersion": "2.0.46.Final",
|
||||
"moduleName": "io.netty:netty-tcnative-classes"
|
||||
},
|
||||
{
|
||||
// Actually Eclipse Public License v2.0
|
||||
"moduleLicense": null,
|
||||
"moduleName": "org.junit:junit-bom"
|
||||
},
|
||||
{
|
||||
"moduleLicense": "The W3C Software License"
|
||||
"moduleVersion": "2.10.0",
|
||||
"moduleName": "com.google.gwt:gwt-user"
|
||||
},
|
||||
{
|
||||
// "Apache License, Version 2.0".
|
||||
"moduleLicense": null,
|
||||
"moduleName": "com.squareup.okhttp3:okhttp"
|
||||
},
|
||||
{
|
||||
// "Apache License, Version 2.0".
|
||||
"moduleLicense": null,
|
||||
"moduleVersion": "1.15.1",
|
||||
"moduleName": "com.squareup:kotlinpoet"
|
||||
},
|
||||
{
|
||||
// "Apache License, Version 2.0".
|
||||
"moduleLicense": null,
|
||||
"moduleName": "com.squareup.okio:okio"
|
||||
},
|
||||
{
|
||||
"moduleLicense": "(GPL-2.0-only WITH Classpath-exception-2.0)",
|
||||
"moduleName": "io.github.eisop:dataflow-errorprone"
|
||||
},
|
||||
{
|
||||
"moduleLicense": "GNU General Public License, version 2 (GPL2), with the classpath exception",
|
||||
"moduleName": "io.github.eisop:dataflow-errorprone"
|
||||
},
|
||||
{
|
||||
// "Apache License, Version 2.0".
|
||||
"moduleLicense": null,
|
||||
@@ -351,15 +360,27 @@
|
||||
"moduleName": "com.squareup.wire:wire-schema"
|
||||
},
|
||||
{
|
||||
// "Apache License, Version 2.0".
|
||||
// "Apache License, Version 2.0". The plugin is able to parse up to
|
||||
// 2.0.33.Final but not this verson.
|
||||
"moduleLicense": null,
|
||||
"moduleVersion": "1.15.1",
|
||||
"moduleName": "com.squareup:kotlinpoet"
|
||||
"moduleVersion": "2.0.46.Final",
|
||||
"moduleName": "io.netty:netty-tcnative-classes"
|
||||
},
|
||||
// "Apache License, Version 2.0".
|
||||
{
|
||||
"moduleLicense": null,
|
||||
"moduleName": "io.opentelemetry:opentelemetry-bom"
|
||||
},
|
||||
{
|
||||
"moduleLicense": "Apache License Version 2.0",
|
||||
"moduleVersion": "3.0.0.M2",
|
||||
"moduleName": "io.apicurio:apicurio-registry-protobuf-schema-utilities"
|
||||
// "Apache License, Version 2.0".
|
||||
"moduleLicense": null,
|
||||
"moduleVersion": "1.4",
|
||||
"moduleName": "jakarta-regexp:jakarta-regexp"
|
||||
},
|
||||
{
|
||||
// Actually Eclipse Public License v2.0
|
||||
"moduleLicense": null,
|
||||
"moduleName": "org.junit:junit-bom"
|
||||
},
|
||||
{
|
||||
// "Apache License, Version 2.0".
|
||||
@@ -390,12 +411,6 @@
|
||||
"moduleLicense": null,
|
||||
"moduleVersion": "1.0.1",
|
||||
"moduleName": "org.jetbrains.kotlinx:kotlinx-serialization-core"
|
||||
},
|
||||
{
|
||||
// "Apache License, Version 2.0".
|
||||
"moduleLicense": null,
|
||||
"moduleVersion": "1.4",
|
||||
"moduleName": "jakarta-regexp:jakarta-regexp"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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/
|
||||
# System.(out|err).println should only appear in tools/ or load-testing/
|
||||
PresubmitCheck(
|
||||
r".*\bSystem\.(out|err)\.print", "java", {
|
||||
"StackdriverDashboardBuilder.java", "/tools/", "/example/",
|
||||
"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,47 +138,12 @@ 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".*org\.testcontainers\.shaded.*",
|
||||
r".*\nimport (static )?.*\.shaded\..*",
|
||||
"java",
|
||||
{"/node_modules/"},
|
||||
):
|
||||
"Do not use shaded dependencies from testcontainers.",
|
||||
"Do not use shaded dependencies",
|
||||
PresubmitCheck(
|
||||
r".*com\.google\.common\.truth\.Truth8.*",
|
||||
"java",
|
||||
@@ -190,7 +156,30 @@ PRESUBMITS = {
|
||||
{"/node_modules/", "JpaTransactionManagerImpl.java"},
|
||||
):
|
||||
"Do not use java.util.Date. Use classes in java.time package instead.",
|
||||
|
||||
PresubmitCheck(
|
||||
r".*com\.google\.api\.client\.http\.HttpStatusCodes.*",
|
||||
"java",
|
||||
{"/node_modules/"},
|
||||
):
|
||||
"Use status code from jakarta.servlet.http.HttpServletResponse.",
|
||||
PresubmitCheck(
|
||||
r".*mock\(Response\.class\).*",
|
||||
"java",
|
||||
{"/node_modules/"},
|
||||
):
|
||||
"Do not mock Response, use FakeResponse.",
|
||||
PresubmitCheck(
|
||||
r".*javax\.servlet\..*",
|
||||
"java",
|
||||
{"/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
|
||||
@@ -278,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:
|
||||
@@ -322,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)
|
||||
|
||||
4
console-webapp/.gitignore
vendored
4
console-webapp/.gitignore
vendored
@@ -41,3 +41,7 @@ testem.log
|
||||
# System files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Build artifact
|
||||
/staged/dist
|
||||
/staged/console-*
|
||||
|
||||
@@ -9,11 +9,11 @@ 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:
|
||||
|
||||
1) Console webapp build script `buildConsoleWebappProd`, which installs
|
||||
1) Console webapp build script `buildConsoleWebapp`, which installs
|
||||
dependencies, assembles a compiled ts -> js, minified, optimized static
|
||||
artifact (html, css, js)
|
||||
2) Artifact assembled in step 1 then gets copied to core project web artifact
|
||||
|
||||
@@ -15,12 +15,16 @@
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
"builder": "@angular-devkit/build-angular:application",
|
||||
"options": {
|
||||
"outputPath": "dist/console-webapp",
|
||||
"outputPath": {
|
||||
"base": "staged/dist/",
|
||||
"browser": ""
|
||||
},
|
||||
"index": "src/index.html",
|
||||
"main": "src/main.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"polyfills": [
|
||||
"src/polyfills.ts"
|
||||
],
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": [
|
||||
@@ -34,7 +38,8 @@
|
||||
"stylePreprocessorOptions": {
|
||||
"includePaths": ["node_modules/"]
|
||||
},
|
||||
"scripts": []
|
||||
"scripts": [],
|
||||
"browser": "src/main.ts"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
@@ -58,13 +63,50 @@
|
||||
],
|
||||
"outputHashing": "all"
|
||||
},
|
||||
"development": {
|
||||
"buildOptimizer": false,
|
||||
"sandbox": {
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "2mb",
|
||||
"maximumError": "5mb"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "2kb",
|
||||
"maximumError": "4kb"
|
||||
}
|
||||
],
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.sandbox.ts"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all"
|
||||
},
|
||||
"crash": {
|
||||
"optimization": false,
|
||||
"vendorChunk": true,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true,
|
||||
"namedChunks": true
|
||||
},
|
||||
"alpha": {
|
||||
"optimization": false,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true,
|
||||
"namedChunks": true
|
||||
},
|
||||
"qa": {
|
||||
"optimization": false,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true,
|
||||
"namedChunks": true
|
||||
},
|
||||
"development": {
|
||||
"optimization": false,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true,
|
||||
"namedChunks": true,
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
@@ -75,6 +117,18 @@
|
||||
"production": {
|
||||
"buildTarget": "console-webapp:build:production"
|
||||
},
|
||||
"alpha": {
|
||||
"buildTarget": "console-webapp:build:alpha"
|
||||
},
|
||||
"crash": {
|
||||
"buildTarget": "console-webapp:build:crash"
|
||||
},
|
||||
"sandbox": {
|
||||
"buildTarget": "console-webapp:build:sandbox"
|
||||
},
|
||||
"qa": {
|
||||
"buildTarget": "console-webapp:build:qa"
|
||||
},
|
||||
"development": {
|
||||
"buildTarget": "console-webapp:build:development"
|
||||
}
|
||||
|
||||
@@ -11,12 +11,12 @@
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
def consoleDir = "${rootDir}/console-webapp"
|
||||
def projectParam = "--project=${rootProject.gcpProject}"
|
||||
|
||||
clean {
|
||||
delete "${consoleDir}/node_modules"
|
||||
delete "${consoleDir}/dist"
|
||||
delete "${consoleDir}/staged/dist"
|
||||
}
|
||||
|
||||
task npmInstallDeps(type: Exec) {
|
||||
@@ -37,19 +37,58 @@ task runConsoleWebappUnitTests(type: Exec) {
|
||||
args 'run', 'test'
|
||||
}
|
||||
|
||||
task buildConsoleWebappNonProd(type: Exec) {
|
||||
task buildConsoleWebapp(type: Exec) {
|
||||
workingDir "${consoleDir}/"
|
||||
executable 'npm'
|
||||
args 'run', 'build'
|
||||
def configuration = project.getProperty('configuration')
|
||||
args 'run', "build", "--configuration=${configuration}"
|
||||
doFirst {
|
||||
println "Building console for environment: ${configuration}"
|
||||
}
|
||||
}
|
||||
|
||||
// Keeping the same as non prod for now before we figure out optimization we want to include
|
||||
task buildConsoleWebappProd(type: Exec) {
|
||||
workingDir "${consoleDir}/"
|
||||
executable 'npm'
|
||||
args 'run', 'build'
|
||||
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'
|
||||
@@ -62,8 +101,9 @@ task checkFormatting(type: Exec) {
|
||||
args 'run', 'prettify:check'
|
||||
}
|
||||
|
||||
tasks.buildConsoleWebapp.dependsOn(tasks.npmInstallDeps)
|
||||
tasks.runConsoleWebappUnitTests.dependsOn(tasks.npmInstallDeps)
|
||||
tasks.buildConsoleWebappProd.dependsOn(tasks.npmInstallDeps)
|
||||
tasks.applyFormatting.dependsOn(tasks.npmInstallDeps)
|
||||
tasks.checkFormatting.dependsOn(tasks.npmInstallDeps)
|
||||
tasks.build.dependsOn(tasks.checkFormatting)
|
||||
tasks.build.dependsOn(tasks.runConsoleWebappUnitTests)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"/console-api":
|
||||
{
|
||||
"target": "http://localhost:8080",
|
||||
"target": "http://[::1]:8080",
|
||||
"secure": false,
|
||||
"logLevel": "debug",
|
||||
"changeOrigin": true
|
||||
|
||||
@@ -34,14 +34,14 @@ net.sf.saxon:Saxon-HE:10.6=checkstyle
|
||||
org.antlr:antlr4-runtime:4.9.3=checkstyle
|
||||
org.checkerframework:checker-qual:3.12.0=checkstyle
|
||||
org.checkerframework:checker-qual:3.33.0=annotationProcessor,errorprone,testAnnotationProcessor
|
||||
org.jacoco:org.jacoco.agent:0.8.11=jacocoAgent,jacocoAnt
|
||||
org.jacoco:org.jacoco.ant:0.8.11=jacocoAnt
|
||||
org.jacoco:org.jacoco.core:0.8.11=jacocoAnt
|
||||
org.jacoco:org.jacoco.report:0.8.11=jacocoAnt
|
||||
org.jacoco:org.jacoco.agent:0.8.12=jacocoAgent,jacocoAnt
|
||||
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.ow2.asm:asm-commons:9.6=jacocoAnt
|
||||
org.ow2.asm:asm-tree:9.6=jacocoAnt
|
||||
org.ow2.asm:asm:9.6=jacocoAnt
|
||||
org.ow2.asm:asm-commons:9.7=jacocoAnt
|
||||
org.ow2.asm:asm-tree:9.7=jacocoAnt
|
||||
org.ow2.asm:asm:9.7=jacocoAnt
|
||||
org.pcollections:pcollections:3.1.4=annotationProcessor,errorprone,testAnnotationProcessor
|
||||
org.reflections:reflections:0.10.2=checkstyle
|
||||
empty=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
|
||||
|
||||
1
console-webapp/gradle.properties
Normal file
1
console-webapp/gradle.properties
Normal file
@@ -0,0 +1 @@
|
||||
configuration=production
|
||||
@@ -3,6 +3,17 @@
|
||||
|
||||
module.exports = function (config) {
|
||||
config.set({
|
||||
customLaunchers: {
|
||||
ChromeHeadless: {
|
||||
base: 'Chrome',
|
||||
flags: [
|
||||
'--no-sandbox',
|
||||
'--disable-gpu',
|
||||
'--headless',
|
||||
'--remote-debugging-port=9222'
|
||||
]
|
||||
}
|
||||
},
|
||||
basePath: '',
|
||||
frameworks: ['jasmine', '@angular-devkit/build-angular'],
|
||||
plugins: [
|
||||
|
||||
12815
console-webapp/package-lock.json
generated
12815
console-webapp/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,9 +4,9 @@
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve --proxy-config dev-proxy.config.json",
|
||||
"build": "ng build --base-href=/console/",
|
||||
"build": "ng build --base-href=/console/ --configuration=$npm_config_configuration",
|
||||
"build:local": "ng build --base-href=/default/console/",
|
||||
"watch": "ng build --watch --configuration development",
|
||||
"watch": "ng build --watch --configuration=development",
|
||||
"test": "ng test --browsers=ChromeHeadless --watch=false",
|
||||
"run:dev": "",
|
||||
"prettify": "npx prettier --write ./src/",
|
||||
@@ -16,31 +16,31 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^17.3.5",
|
||||
"@angular/cdk": "^17.3.5",
|
||||
"@angular/common": "^17.3.5",
|
||||
"@angular/compiler": "^17.3.5",
|
||||
"@angular/core": "^17.3.5",
|
||||
"@angular/forms": "^17.3.5",
|
||||
"@angular/material": "^17.3.5",
|
||||
"@angular/platform-browser": "^17.3.5",
|
||||
"@angular/platform-browser-dynamic": "^17.3.5",
|
||||
"@angular/router": "^17.3.5",
|
||||
"@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": "^17.3.5",
|
||||
"@angular-eslint/builder": "17.3.0",
|
||||
"@angular-eslint/eslint-plugin": "17.3.0",
|
||||
"@angular-eslint/eslint-plugin-template": "17.3.0",
|
||||
"@angular-eslint/schematics": "17.3.0",
|
||||
"@angular-eslint/template-parser": "17.3.0",
|
||||
"@angular/cli": "~17.3.5",
|
||||
"@angular/compiler-cli": "^17.3.5",
|
||||
"@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.2.2"
|
||||
"typescript": "^5.7.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,22 +17,47 @@ import { Route, RouterModule } from '@angular/router';
|
||||
import { BillingInfoComponent } from './billingInfo/billingInfo.component';
|
||||
import { DomainListComponent } from './domains/domainList.component';
|
||||
import { HomeComponent } from './home/home.component';
|
||||
import { RegistryLockVerifyComponent } from './lock/registryLockVerify.component';
|
||||
import { RegistrarDetailsComponent } from './registrar/registrarDetails.component';
|
||||
import { RegistrarComponent } from './registrar/registrarsTable.component';
|
||||
import { ResourcesComponent } from './resources/resources.component';
|
||||
import ContactComponent from './settings/contact/contact.component';
|
||||
import SecurityComponent from './settings/security/security.component';
|
||||
import { SettingsComponent } from './settings/settings.component';
|
||||
import UsersComponent from './settings/users/users.component';
|
||||
import WhoisComponent from './settings/whois/whois.component';
|
||||
import { SupportComponent } from './support/support.component';
|
||||
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;
|
||||
}
|
||||
|
||||
export const PATHS = {
|
||||
NewOteComponent: 'new-ote',
|
||||
OteStatusComponent: 'ote-status/:registrarId',
|
||||
UsersComponent: 'users',
|
||||
};
|
||||
export const routes: RouteWithIcon[] = [
|
||||
{ path: '', redirectTo: '/home', pathMatch: 'full' },
|
||||
{
|
||||
path: PasswordResetVerifyComponent.PATH,
|
||||
component: PasswordResetVerifyComponent,
|
||||
},
|
||||
{
|
||||
path: RegistryLockVerifyComponent.PATH,
|
||||
component: RegistryLockVerifyComponent,
|
||||
},
|
||||
{
|
||||
path: PATHS.NewOteComponent,
|
||||
loadComponent: () =>
|
||||
import('./ote/newOte.component').then((mod) => mod.NewOteComponent),
|
||||
},
|
||||
{
|
||||
path: PATHS.OteStatusComponent,
|
||||
loadComponent: () =>
|
||||
import('./ote/oteStatus.component').then((mod) => mod.OteStatusComponent),
|
||||
},
|
||||
{ path: 'registrars', component: RegistrarComponent },
|
||||
{
|
||||
path: 'home',
|
||||
@@ -40,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,
|
||||
@@ -64,19 +94,15 @@ 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,
|
||||
component: SecurityComponent,
|
||||
title: 'Security',
|
||||
},
|
||||
{
|
||||
path: UsersComponent.PATH,
|
||||
component: UsersComponent,
|
||||
},
|
||||
],
|
||||
},
|
||||
// {
|
||||
@@ -107,6 +133,13 @@ export const routes: RouteWithIcon[] = [
|
||||
title: 'Resources',
|
||||
iconName: 'description',
|
||||
},
|
||||
{
|
||||
path: PATHS.UsersComponent,
|
||||
title: 'Users',
|
||||
iconName: 'manage_accounts',
|
||||
loadComponent: () =>
|
||||
import('./users/users.component').then((mod) => mod.UsersComponent),
|
||||
},
|
||||
{
|
||||
path: SupportComponent.PATH,
|
||||
component: SupportComponent,
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
<div class="console-app mat-typography">
|
||||
<app-header (toggleNavOpen)="toggleSidenav()"></app-header>
|
||||
<div class="console-app__global-spinner">
|
||||
<mat-progress-bar
|
||||
mode="indeterminate"
|
||||
*ngIf="globalLoader.isLoading"
|
||||
></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 *ngIf="globalLoader.isLoading" class="console-app__global-spinner">
|
||||
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
|
||||
</div>
|
||||
<div class="console-app__content">
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
</mat-sidenav-content>
|
||||
</mat-sidenav-container>
|
||||
</div>
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
padding: 0 16px;
|
||||
}
|
||||
&__global-spinner {
|
||||
margin-bottom: 2rem;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,31 +12,126 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
import { provideHttpClientTesting } from '@angular/common/http/testing';
|
||||
import { ComponentFixture, fakeAsync, TestBed } from '@angular/core/testing';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { AppComponent } from './app.component';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { MaterialModule } from './material.module';
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
import { BackendService } from './shared/services/backend.service';
|
||||
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({
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
RouterTestingModule,
|
||||
MaterialModule,
|
||||
BrowserAnimationsModule,
|
||||
MatSidenavModule,
|
||||
NoopAnimationsModule,
|
||||
MatSnackBarModule,
|
||||
AppModule,
|
||||
RouterModule.forRoot(routes),
|
||||
],
|
||||
providers: [
|
||||
{ provide: RegistrarService, useValue: mockRegistrarService },
|
||||
{ provide: UserDataService, useValue: mockUserDataService },
|
||||
{ provide: MatSnackBar, useValue: mockSnackBar },
|
||||
{ provide: PocReminderComponent, useClass: dummyPocReminderComponent },
|
||||
provideHttpClient(),
|
||||
provideHttpClientTesting(),
|
||||
],
|
||||
providers: [BackendService],
|
||||
declarations: [AppComponent],
|
||||
}).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) => {
|
||||
|
||||
@@ -23,13 +23,20 @@ import { MaterialModule } from './material.module';
|
||||
|
||||
import { BackendService } from './shared/services/backend.service';
|
||||
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
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';
|
||||
import { RegistryLockVerifyComponent } from './lock/registryLockVerify.component';
|
||||
import { NavigationComponent } from './navigation/navigation.component';
|
||||
import NewRegistrarComponent from './registrar/newRegistrar.component';
|
||||
import { RegistrarDetailsComponent } from './registrar/registrarDetails.component';
|
||||
import { RegistrarSelectorComponent } from './registrar/registrarSelector.component';
|
||||
import { RegistrarComponent } from './registrar/registrarsTable.component';
|
||||
@@ -40,17 +47,31 @@ 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';
|
||||
import { UserLevelVisibility } from './shared/directives/userLevelVisiblity.directive';
|
||||
import { BreakPointObserverService } from './shared/services/breakPoint.service';
|
||||
import { GlobalLoaderService } from './shared/services/globalLoader.service';
|
||||
import { UserDataService } from './shared/services/userData.service';
|
||||
import { SnackBarModule } from './snackbar.module';
|
||||
import { SupportComponent } from './support/support.component';
|
||||
import { TldsComponent } from './tlds/tlds.component';
|
||||
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],
|
||||
imports: [MaterialModule],
|
||||
exports: [SelectedRegistrarWrapper],
|
||||
providers: [],
|
||||
})
|
||||
export class SelectedRegistrarModule {}
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@@ -59,32 +80,43 @@ import { TldsComponent } from './tlds/tlds.component';
|
||||
ContactDetailsComponent,
|
||||
DomainListComponent,
|
||||
EppPasswordEditComponent,
|
||||
ForceFocusDirective,
|
||||
HeaderComponent,
|
||||
HistoryComponent,
|
||||
HistoryListComponent,
|
||||
HomeComponent,
|
||||
LocationBackDirective,
|
||||
NavigationComponent,
|
||||
NewRegistrarComponent,
|
||||
NotificationsComponent,
|
||||
PasswordInputForm,
|
||||
PasswordResetVerifyComponent,
|
||||
PocReminderComponent,
|
||||
RdapComponent,
|
||||
RdapEditComponent,
|
||||
ReasonDialogComponent,
|
||||
RegistrarComponent,
|
||||
RegistrarDetailsComponent,
|
||||
RegistrarSelectorComponent,
|
||||
RegistryLockComponent,
|
||||
RegistryLockVerifyComponent,
|
||||
ResourcesComponent,
|
||||
ResponseDialogComponent,
|
||||
SecurityComponent,
|
||||
SecurityEditComponent,
|
||||
SelectedRegistrarWrapper,
|
||||
SettingsComponent,
|
||||
SettingsContactComponent,
|
||||
SupportComponent,
|
||||
TldsComponent,
|
||||
WhoisComponent,
|
||||
WhoisEditComponent,
|
||||
UserLevelVisibility,
|
||||
],
|
||||
bootstrap: [AppComponent],
|
||||
imports: [
|
||||
AppRoutingModule,
|
||||
BrowserAnimationsModule,
|
||||
BrowserModule,
|
||||
FormsModule,
|
||||
HttpClientModule,
|
||||
MaterialModule,
|
||||
SelectedRegistrarModule,
|
||||
SnackBarModule,
|
||||
],
|
||||
providers: [
|
||||
@@ -98,7 +130,7 @@ import { TldsComponent } from './tlds/tlds.component';
|
||||
subscriptSizing: 'dynamic',
|
||||
},
|
||||
},
|
||||
provideHttpClient(),
|
||||
],
|
||||
bootstrap: [AppComponent],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,28 +14,35 @@
|
||||
|
||||
import { Component, computed } from '@angular/core';
|
||||
import { RegistrarService } from '../registrar/registrar.service';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
|
||||
@Component({
|
||||
selector: 'app-billingInfo',
|
||||
templateUrl: './billingInfo.component.html',
|
||||
styleUrls: ['./billingInfo.component.scss'],
|
||||
standalone: false,
|
||||
})
|
||||
export class BillingInfoComponent {
|
||||
public static PATH = 'billingInfo';
|
||||
constructor(public registrarService: RegistrarService) {}
|
||||
constructor(
|
||||
public registrarService: RegistrarService,
|
||||
private _snackBar: MatSnackBar
|
||||
) {}
|
||||
|
||||
driveFolderUrl = computed<string>(() => {
|
||||
if (this.registrarService.registrar()?.driveFolderId) {
|
||||
return `https://drive.google.com/drive/folders/${
|
||||
return (
|
||||
'https://drive.google.com/drive/folders/' +
|
||||
this.registrarService.registrar()?.driveFolderId
|
||||
}`;
|
||||
);
|
||||
}
|
||||
return '';
|
||||
});
|
||||
|
||||
openBillingDetails(e: MouseEvent) {
|
||||
if (!this.driveFolderUrl()) {
|
||||
if (!this.registrarService.registrar()?.driveFolderId) {
|
||||
e.preventDefault();
|
||||
this._snackBar.open('Billing Folder ID has not been assigned');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,19 @@
|
||||
<app-selected-registrar-wrapper>
|
||||
<div class="console-domains">
|
||||
<h1 class="mat-headline-4">Domains</h1>
|
||||
@if (totalResults === 0) {
|
||||
<div class="console-app-domains">
|
||||
<h1 class="mat-headline-4" forceFocus>Domains</h1>
|
||||
|
||||
<div
|
||||
class="console-app-domains__actions-wrapper"
|
||||
[hidden]="!domainListService.activeActionComponent"
|
||||
>
|
||||
<ng-container
|
||||
v-if="domainListService.activeActionComponent"
|
||||
*ngComponentOutlet="domainListService.activeActionComponent"
|
||||
>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
@if (!isLoading && totalResults == 0) {
|
||||
<div class="console-app__empty-domains">
|
||||
<h1>
|
||||
<mat-icon class="console-app__empty-domains-icon secondary-text"
|
||||
@@ -10,76 +22,209 @@
|
||||
</h1>
|
||||
<h1>No domains found</h1>
|
||||
</div>
|
||||
} @else if(isLoading) {
|
||||
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
|
||||
} @else {
|
||||
<a
|
||||
mat-stroked-button
|
||||
color="primary"
|
||||
href="/console-api/dum-download?registrarId={{
|
||||
registrarService.registrarId()
|
||||
}}"
|
||||
class="console-app-domains__download"
|
||||
<mat-menu #actions="matMenu">
|
||||
<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
|
||||
class="console-app__domains-table-parent"
|
||||
[hidden]="domainListService.activeActionComponent"
|
||||
>
|
||||
<mat-icon>download</mat-icon>
|
||||
Download domains (.csv)
|
||||
</a>
|
||||
<mat-form-field class="console-app__domains-filter">
|
||||
<mat-label>Filter</mat-label>
|
||||
<input
|
||||
type="search"
|
||||
matInput
|
||||
[(ngModel)]="searchTerm"
|
||||
(ngModelChange)="sendInput()"
|
||||
#input
|
||||
/>
|
||||
</mat-form-field>
|
||||
<mat-table
|
||||
[dataSource]="dataSource"
|
||||
class="mat-elevation-z0"
|
||||
class="console-app__domains-table"
|
||||
>
|
||||
<ng-container matColumnDef="domainName">
|
||||
<mat-header-cell *matHeaderCellDef>Domain Name</mat-header-cell>
|
||||
<mat-cell *matCellDef="let element">{{ element.domainName }}</mat-cell>
|
||||
</ng-container>
|
||||
<div class="console-app__scrollable-wrapper">
|
||||
<div class="console-app__scrollable">
|
||||
@if (isLoading) {
|
||||
<div class="console-app__domains-spinner">
|
||||
<mat-spinner />
|
||||
</div>
|
||||
}
|
||||
<a
|
||||
mat-stroked-button
|
||||
color="primary"
|
||||
href="/console-api/dum-download?registrarId={{
|
||||
registrarService.registrarId()
|
||||
}}"
|
||||
class="console-app-domains__download"
|
||||
>
|
||||
<mat-icon>download</mat-icon>
|
||||
Download domains (.csv)
|
||||
</a>
|
||||
|
||||
<ng-container matColumnDef="creationTime">
|
||||
<mat-header-cell *matHeaderCellDef>Creation Time</mat-header-cell>
|
||||
<mat-cell *matCellDef="let element">
|
||||
{{ element.creationTime.creationTime }}
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
<mat-form-field class="console-app__domains-filter">
|
||||
<mat-label>Filter</mat-label>
|
||||
<input
|
||||
type="search"
|
||||
matInput
|
||||
[(ngModel)]="searchTerm"
|
||||
(ngModelChange)="sendInput()"
|
||||
#input
|
||||
/>
|
||||
</mat-form-field>
|
||||
|
||||
<ng-container matColumnDef="registrationExpirationTime">
|
||||
<mat-header-cell *matHeaderCellDef>Expiration Time</mat-header-cell>
|
||||
<mat-cell *matCellDef="let element">
|
||||
{{ element.registrationExpirationTime }}
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
<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>
|
||||
|
||||
<ng-container matColumnDef="statuses">
|
||||
<mat-header-cell *matHeaderCellDef>Statuses</mat-header-cell>
|
||||
<mat-cell *matCellDef="let element">{{ element.statuses }}</mat-cell>
|
||||
</ng-container>
|
||||
<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>
|
||||
|
||||
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
|
||||
<mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>
|
||||
<ng-container matColumnDef="domainName">
|
||||
<mat-header-cell *matHeaderCellDef>Domain Name</mat-header-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>
|
||||
|
||||
<!-- Row shown when there is no matching data. -->
|
||||
<mat-row *matNoDataRow>
|
||||
<mat-cell colspan="4">No domains found</mat-cell>
|
||||
</mat-row>
|
||||
</mat-table>
|
||||
<mat-paginator
|
||||
[length]="totalResults"
|
||||
[pageIndex]="pageNumber"
|
||||
[pageSize]="resultsPerPage"
|
||||
[pageSizeOptions]="[10, 25, 50, 100, 500]"
|
||||
(page)="onPageChange($event)"
|
||||
aria-label="Select page of domain results"
|
||||
showFirstLastButtons
|
||||
></mat-paginator>
|
||||
<ng-container matColumnDef="creationTime">
|
||||
<mat-header-cell *matHeaderCellDef>Creation Time</mat-header-cell>
|
||||
<mat-cell *matCellDef="let element">
|
||||
{{ element.creationTime.creationTime }}
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="registrationExpirationTime">
|
||||
<mat-header-cell *matHeaderCellDef
|
||||
>Expiration Time</mat-header-cell
|
||||
>
|
||||
<mat-cell *matCellDef="let element">
|
||||
{{ element.registrationExpirationTime }}
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="statuses">
|
||||
<mat-header-cell *matHeaderCellDef>Statuses</mat-header-cell>
|
||||
<mat-cell *matCellDef="let element">
|
||||
<span>{{ element.statuses?.join(", ") }}</span>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="registryLock">
|
||||
<mat-header-cell *matHeaderCellDef
|
||||
>Registry-Locked</mat-header-cell
|
||||
>
|
||||
<mat-cell *matCellDef="let element">{{
|
||||
isDomainLocked(element.domainName)
|
||||
}}</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions">
|
||||
<mat-header-cell *matHeaderCellDef>Actions</mat-header-cell>
|
||||
<mat-cell *matCellDef="let element">
|
||||
<button
|
||||
mat-icon-button
|
||||
[matMenuTriggerFor]="actions"
|
||||
[matMenuTriggerData]="{
|
||||
domainName: element.domainName,
|
||||
domain: element
|
||||
}"
|
||||
aria-label="Domain actions"
|
||||
>
|
||||
<mat-icon>more_horiz</mat-icon>
|
||||
</button>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<mat-header-row
|
||||
*matHeaderRowDef="displayedColumns"
|
||||
></mat-header-row>
|
||||
<mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>
|
||||
|
||||
<!-- Row shown when there is no matching data. -->
|
||||
<mat-row *matNoDataRow>
|
||||
<mat-cell colspan="6">No domains found</mat-cell>
|
||||
</mat-row>
|
||||
</mat-table>
|
||||
<mat-paginator
|
||||
[length]="totalResults"
|
||||
[pageIndex]="pageNumber"
|
||||
[pageSize]="resultsPerPage"
|
||||
[pageSizeOptions]="[10, 25, 50, 100, 500]"
|
||||
(page)="onPageChange($event)"
|
||||
aria-label="Select page of domain results"
|
||||
showFirstLastButtons
|
||||
></mat-paginator>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</app-selected-registrar-wrapper>
|
||||
|
||||
@@ -12,18 +12,26 @@
|
||||
}
|
||||
}
|
||||
|
||||
&-domains {
|
||||
position: relative;
|
||||
&__download {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
&__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 {
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
&-domains__download {
|
||||
position: absolute;
|
||||
top: -55px;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
&__domains-filter {
|
||||
@@ -31,8 +39,51 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__domains-table-parent {
|
||||
position: relative;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
&__domains-table {
|
||||
min-width: $min-width !important;
|
||||
.mat-column-actions {
|
||||
max-width: 100px;
|
||||
}
|
||||
.mat-column-registryLock {
|
||||
max-width: 150px;
|
||||
}
|
||||
.mat-column-statuses span {
|
||||
padding: 10px 0;
|
||||
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 {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.mat-mdc-paginator {
|
||||
|
||||
@@ -14,11 +14,14 @@
|
||||
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { DomainListComponent } from './domainList.component';
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
import { provideHttpClientTesting } from '@angular/common/http/testing';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { MaterialModule } from '../material.module';
|
||||
import { BackendService } from '../shared/services/backend.service';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { DomainListComponent } from './domainList.component';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { AppModule } from '../app.module';
|
||||
|
||||
describe('DomainListComponent', () => {
|
||||
let component: DomainListComponent;
|
||||
@@ -28,11 +31,16 @@ describe('DomainListComponent', () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [DomainListComponent],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
MaterialModule,
|
||||
BrowserAnimationsModule,
|
||||
FormsModule,
|
||||
AppModule,
|
||||
],
|
||||
providers: [
|
||||
BackendService,
|
||||
provideHttpClient(),
|
||||
provideHttpClientTesting(),
|
||||
],
|
||||
providers: [BackendService],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(DomainListComponent);
|
||||
|
||||
@@ -12,34 +12,133 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { Component, ViewChild, effect } from '@angular/core';
|
||||
import { SelectionModel } from '@angular/cdk/collections';
|
||||
import { HttpErrorResponse, HttpStatusCode } from '@angular/common/http';
|
||||
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 { BackendService } from '../shared/services/backend.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'],
|
||||
providers: [DomainListService],
|
||||
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',
|
||||
'statuses',
|
||||
'registryLock',
|
||||
'actions',
|
||||
];
|
||||
|
||||
dataSource: MatTableDataSource<Domain> = new MatTableDataSource();
|
||||
selection = new SelectionModel<Domain>(true, [], undefined, this.isChecked());
|
||||
isLoading = true;
|
||||
|
||||
searchTermSubject = new Subject<string>();
|
||||
@@ -49,16 +148,24 @@ export class DomainListComponent {
|
||||
resultsPerPage = 50;
|
||||
totalResults?: number = 0;
|
||||
|
||||
reason: string = '';
|
||||
|
||||
operationResult: DomainData | undefined;
|
||||
|
||||
@ViewChild(MatPaginator, { static: true }) paginator!: MatPaginator;
|
||||
|
||||
constructor(
|
||||
private backendService: BackendService,
|
||||
private domainListService: DomainListService,
|
||||
protected domainListService: DomainListService,
|
||||
protected registrarService: RegistrarService,
|
||||
private _snackBar: MatSnackBar
|
||||
protected registryLockService: RegistryLockService,
|
||||
private _snackBar: MatSnackBar,
|
||||
private dialog: MatDialog
|
||||
) {
|
||||
effect(() => {
|
||||
this.pageNumber = 0;
|
||||
this.totalResults = 0;
|
||||
if (this.registrarService.registrarId()) {
|
||||
this.loadLocks();
|
||||
this.reloadData();
|
||||
}
|
||||
});
|
||||
@@ -78,6 +185,28 @@ export class DomainListComponent {
|
||||
this.searchTermSubject.complete();
|
||||
}
|
||||
|
||||
openRegistryLock(domainName: string) {
|
||||
this.domainListService.selectedDomain = domainName;
|
||||
this.domainListService.activeActionComponent = RegistryLockComponent;
|
||||
}
|
||||
|
||||
loadLocks() {
|
||||
this.registryLockService.retrieveLocks().subscribe({
|
||||
error: (err: HttpErrorResponse) => {
|
||||
if (err.status !== HttpStatusCode.Forbidden) {
|
||||
// Some users may not have registry lock permissions and that's OK
|
||||
this._snackBar.open(err.message);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
isDomainLocked(domainName: string) {
|
||||
return this.registryLockService.domainsLocks.some(
|
||||
(d) => d.domainName === domainName
|
||||
);
|
||||
}
|
||||
|
||||
reloadData() {
|
||||
this.isLoading = true;
|
||||
this.domainListService
|
||||
@@ -93,7 +222,7 @@ export class DomainListComponent {
|
||||
this.isLoading = false;
|
||||
},
|
||||
next: (domainListResult) => {
|
||||
this.dataSource.data = (domainListResult || {}).domains;
|
||||
this.dataSource.data = this.domainListService.domainsList;
|
||||
this.totalResults = (domainListResult || {}).totalResults || 0;
|
||||
this.isLoading = false;
|
||||
},
|
||||
@@ -107,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
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Injectable, Type } from '@angular/core';
|
||||
import { tap } from 'rxjs';
|
||||
import { RegistrarService } from '../registrar/registrar.service';
|
||||
import { BackendService } from '../shared/services/backend.service';
|
||||
@@ -35,15 +35,25 @@ export interface DomainListResult {
|
||||
totalResults: number;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export enum BULK_ACTION_NAME {
|
||||
DELETE = 'DELETE',
|
||||
SUSPEND = 'SUSPEND',
|
||||
UNSUSPEND = 'UNSUSPEND',
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class DomainListService {
|
||||
checkpointTime?: string;
|
||||
selectedDomain?: string;
|
||||
public activeActionComponent: Type<any> | null = null;
|
||||
public domainsList: Domain[] = [];
|
||||
|
||||
constructor(
|
||||
private backendService: BackendService,
|
||||
private registrarService: RegistrarService
|
||||
) {}
|
||||
|
||||
retrieveDomains(
|
||||
pageNumber?: number,
|
||||
resultsPerPage?: number,
|
||||
@@ -62,7 +72,22 @@ export class DomainListService {
|
||||
.pipe(
|
||||
tap((domainListResult: DomainListResult) => {
|
||||
this.checkpointTime = domainListResult?.checkpointTime;
|
||||
this.domainsList = domainListResult?.domains;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
bulkDomainAction(
|
||||
domains: string[],
|
||||
reason: string,
|
||||
registrarId: string,
|
||||
actionName: BULK_ACTION_NAME
|
||||
) {
|
||||
return this.backendService.bulkDomainAction(
|
||||
domains,
|
||||
reason,
|
||||
actionName,
|
||||
registrarId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
83
console-webapp/src/app/domains/registryLock.component.html
Normal file
83
console-webapp/src/app/domains/registryLock.component.html
Normal file
@@ -0,0 +1,83 @@
|
||||
<div class="console-app__registry-lock">
|
||||
<p>
|
||||
<button
|
||||
mat-icon-button
|
||||
aria-label="Back to domains list"
|
||||
(click)="goBack()"
|
||||
>
|
||||
<mat-icon>arrow_back</mat-icon>
|
||||
</button>
|
||||
</p>
|
||||
|
||||
@if(!registrarService.registrar()?.registryLockAllowed) {
|
||||
<h1>
|
||||
Sorry, your registrar hasn't enrolled in registry lock yet. To do so, please
|
||||
contact {{ userDataService.userData()?.supportEmail }}.
|
||||
</h1>
|
||||
} @else if (isLocked()) {
|
||||
<h1>Unlock the domain {{ domainListService.selectedDomain }}</h1>
|
||||
<form (ngSubmit)="save(false)" [formGroup]="unlockDomain">
|
||||
<p>
|
||||
<mat-label for="password">Password: </mat-label>
|
||||
<mat-form-field name="password" appearance="outline">
|
||||
<input matInput type="text" formControlName="password" required />
|
||||
</mat-form-field>
|
||||
</p>
|
||||
<p>
|
||||
<mat-label for="relockTime"
|
||||
>Automatically re-lock the domain after:</mat-label
|
||||
>
|
||||
<mat-radio-group
|
||||
name="relockTime"
|
||||
formControlName="relockTime"
|
||||
aria-label="Automatically relock option"
|
||||
>
|
||||
@for (option of relockOptions; track option.name) {
|
||||
<mat-radio-button [value]="option.duration">{{
|
||||
option.name
|
||||
}}</mat-radio-button>
|
||||
}
|
||||
</mat-radio-group>
|
||||
</p>
|
||||
|
||||
<div class="console-app__registry-lock-notification">
|
||||
<mat-icon>priority_high</mat-icon>Confirmation email will be sent to your
|
||||
email address to confirm the unlock
|
||||
</div>
|
||||
<button
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
type="submit"
|
||||
[disabled]="!unlockDomain.valid"
|
||||
aria-label="Submit domain unlock request"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</form>
|
||||
} @else {
|
||||
<h1>Lock the domain {{ domainListService.selectedDomain }}</h1>
|
||||
<form (ngSubmit)="save(true)" [formGroup]="lockDomain">
|
||||
<p>
|
||||
<mat-label for="password">Password: </mat-label>
|
||||
<mat-form-field name="password" appearance="outline">
|
||||
<input matInput type="text" formControlName="password" required />
|
||||
</mat-form-field>
|
||||
</p>
|
||||
|
||||
<div class="console-app__registry-lock-notification">
|
||||
<mat-icon>priority_high</mat-icon>The lock will not take effect until you
|
||||
click the confirmation link that will be emailed to you. When it takes
|
||||
effect, you will be billed the standard server status change billing cost.
|
||||
</div>
|
||||
<button
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
type="submit"
|
||||
[disabled]="!lockDomain.valid"
|
||||
aria-label="Submit domain lock request"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</form>
|
||||
}
|
||||
</div>
|
||||
20
console-webapp/src/app/domains/registryLock.component.scss
Normal file
20
console-webapp/src/app/domains/registryLock.component.scss
Normal file
@@ -0,0 +1,20 @@
|
||||
.console-app {
|
||||
&__registry-lock {
|
||||
mat-label {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
p {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
}
|
||||
&__registry-lock-notification {
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
background-color: var(--light-highlight);
|
||||
margin-bottom: 20px;
|
||||
width: max-content;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
93
console-webapp/src/app/domains/registryLock.component.ts
Normal file
93
console-webapp/src/app/domains/registryLock.component.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { Component, computed } from '@angular/core';
|
||||
import { FormControl, FormGroup } from '@angular/forms';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { RegistrarService } from '../registrar/registrar.service';
|
||||
import { UserDataService } from '../shared/services/userData.service';
|
||||
import { DomainListService } from './domainList.service';
|
||||
import { RegistryLockService } from './registryLock.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-registry-lock',
|
||||
templateUrl: './registryLock.component.html',
|
||||
styleUrls: ['./registryLock.component.scss'],
|
||||
standalone: false,
|
||||
})
|
||||
export class RegistryLockComponent {
|
||||
readonly isLocked = computed(() =>
|
||||
this.registryLockService.domainsLocks.some(
|
||||
(dl) => dl.domainName === this.domainListService.selectedDomain
|
||||
)
|
||||
);
|
||||
|
||||
relockOptions = [
|
||||
{ name: '1 hour', duration: 3600000 },
|
||||
{ name: '6 hours', duration: 21600000 },
|
||||
{ name: '24 hours', duration: 86400000 },
|
||||
{ name: 'Never', duration: undefined },
|
||||
];
|
||||
|
||||
lockDomain = new FormGroup({
|
||||
password: new FormControl(''),
|
||||
});
|
||||
|
||||
unlockDomain = new FormGroup({
|
||||
password: new FormControl(''),
|
||||
relockTime: new FormControl(undefined),
|
||||
});
|
||||
|
||||
constructor(
|
||||
protected registrarService: RegistrarService,
|
||||
protected domainListService: DomainListService,
|
||||
protected registryLockService: RegistryLockService,
|
||||
protected userDataService: UserDataService,
|
||||
private _snackBar: MatSnackBar
|
||||
) {}
|
||||
|
||||
goBack() {
|
||||
this.domainListService.selectedDomain = undefined;
|
||||
this.domainListService.activeActionComponent = null;
|
||||
}
|
||||
|
||||
save(isLock: boolean) {
|
||||
let request;
|
||||
if (!isLock) {
|
||||
request = this.registryLockService.registryLockDomain(
|
||||
this.domainListService.selectedDomain || '',
|
||||
this.unlockDomain.value.password || '',
|
||||
this.unlockDomain.value.relockTime || undefined,
|
||||
isLock
|
||||
);
|
||||
} else {
|
||||
request = this.registryLockService.registryLockDomain(
|
||||
this.domainListService.selectedDomain || '',
|
||||
this.lockDomain.value.password || '',
|
||||
undefined,
|
||||
isLock
|
||||
);
|
||||
}
|
||||
|
||||
request.subscribe({
|
||||
complete: () => {
|
||||
this.goBack();
|
||||
},
|
||||
error: (err: HttpErrorResponse) => {
|
||||
this._snackBar.open(err.error);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
59
console-webapp/src/app/domains/registryLock.service.ts
Normal file
59
console-webapp/src/app/domains/registryLock.service.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { tap } from 'rxjs';
|
||||
import { RegistrarService } from '../registrar/registrar.service';
|
||||
import { BackendService } from '../shared/services/backend.service';
|
||||
|
||||
export interface DomainLocksResult {
|
||||
domainName: string;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class RegistryLockService {
|
||||
public domainsLocks: DomainLocksResult[] = [];
|
||||
|
||||
constructor(
|
||||
private backendService: BackendService,
|
||||
private registrarService: RegistrarService
|
||||
) {}
|
||||
|
||||
retrieveLocks() {
|
||||
return this.backendService
|
||||
.getLocks(this.registrarService.registrarId())
|
||||
.pipe(
|
||||
tap((domainLocksResult) => {
|
||||
this.domainsLocks = domainLocksResult;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
registryLockDomain(
|
||||
domainName: string,
|
||||
password: string,
|
||||
relockDurationMillis: number | undefined,
|
||||
isLock: boolean
|
||||
) {
|
||||
return this.backendService.registryLockDomain(
|
||||
domainName,
|
||||
password,
|
||||
relockDurationMillis,
|
||||
this.registrarService.registrarId(),
|
||||
isLock
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -16,11 +16,11 @@
|
||||
&__logo {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
margin-left: -10px;
|
||||
margin-left: -15px;
|
||||
}
|
||||
&__menu-btn {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
padding: 0;
|
||||
}
|
||||
&__header {
|
||||
@@ -32,9 +32,6 @@
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
}
|
||||
|
||||
&-user-icon {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@
|
||||
<mat-icon class="secondary-text">view_list</mat-icon>
|
||||
DUMs
|
||||
</h3>
|
||||
<p class="secondary-text">View Domains</p>
|
||||
<p class="secondary-text">View Domains Under Management</p>
|
||||
</mat-card-content>
|
||||
<mat-card-actions>
|
||||
<button mat-button color="primary" (click)="viewDums()">
|
||||
@@ -34,7 +34,10 @@
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
|
||||
<mat-card appearance="outlined">
|
||||
<mat-card
|
||||
appearance="outlined"
|
||||
[elementId]="getElementIdForRegistrarsBlock()"
|
||||
>
|
||||
<mat-card-content>
|
||||
<h3>
|
||||
<mat-icon class="secondary-text">account_circle</mat-icon>
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -18,18 +18,23 @@ import { DomainListComponent } from '../domains/domainList.component';
|
||||
import { RegistrarComponent } from '../registrar/registrarsTable.component';
|
||||
import SecurityComponent from '../settings/security/security.component';
|
||||
import { SettingsComponent } from '../settings/settings.component';
|
||||
import { RESTRICTED_ELEMENTS } from '../shared/directives/userLevelVisiblity.directive';
|
||||
import { BreakPointObserverService } from '../shared/services/breakPoint.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-home',
|
||||
templateUrl: './home.component.html',
|
||||
styleUrls: ['./home.component.scss'],
|
||||
standalone: false,
|
||||
})
|
||||
export class HomeComponent {
|
||||
constructor(
|
||||
protected breakPointObserverService: BreakPointObserverService,
|
||||
private router: Router
|
||||
) {}
|
||||
getElementIdForRegistrarsBlock() {
|
||||
return RESTRICTED_ELEMENTS.REGISTRAR_ELEMENT;
|
||||
}
|
||||
viewRegistrars() {
|
||||
this.router.navigate([RegistrarComponent.PATH], {
|
||||
queryParamsHandling: 'merge',
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
@if (isLoading) {
|
||||
<div class="console-app__registry-lock-verify-spinner">
|
||||
<mat-spinner />
|
||||
</div>
|
||||
} @else if (domainName) {
|
||||
<h1 class="mat-headline-4">Success!</h1>
|
||||
<div class="console-app__registry-lock-content">
|
||||
<div class="console-app__registry-lock-subhead">
|
||||
The domain {{ domainName }} has been successfully {{ action }}ed.
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
class="text-l"
|
||||
routerLink="{{ DOMAIN_LIST_COMPONENT_PATH }}"
|
||||
[queryParams]="{ registrarId: this.registrarService.registrarId() }"
|
||||
>Return to the list of domains</a
|
||||
>
|
||||
</div>
|
||||
} @else {
|
||||
<h1 class="mat-headline-4">Failure</h1>
|
||||
<div class="console-app__registry-lock-content">
|
||||
<div class="console-app__registry-lock-subhead">
|
||||
An error occurred: {{ errorMessage }}.<br /><br />Please double-check the
|
||||
verification code and try again.
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
.console-app__registry-lock {
|
||||
&-content {
|
||||
margin-top: 30px;
|
||||
}
|
||||
&-subhead {
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
66
console-webapp/src/app/lock/registryLockVerify.component.ts
Normal file
66
console-webapp/src/app/lock/registryLockVerify.component.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component } from '@angular/core';
|
||||
import { RegistrarService } from '../registrar/registrar.service';
|
||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
||||
import { RegistryLockVerifyService } from './registryLockVerify.service';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { take } from 'rxjs';
|
||||
import { DomainListComponent } from '../domains/domainList.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-registry-lock-verify',
|
||||
templateUrl: './registryLockVerify.component.html',
|
||||
styleUrls: ['./registryLockVerify.component.scss'],
|
||||
providers: [RegistryLockVerifyService],
|
||||
standalone: false,
|
||||
})
|
||||
export class RegistryLockVerifyComponent {
|
||||
public static PATH = 'registry-lock-verify';
|
||||
|
||||
readonly DOMAIN_LIST_COMPONENT_PATH = `/${DomainListComponent.PATH}`;
|
||||
|
||||
isLoading = true;
|
||||
domainName?: string;
|
||||
action?: string;
|
||||
errorMessage?: string;
|
||||
|
||||
constructor(
|
||||
protected registrarService: RegistrarService,
|
||||
protected registryLockVerifyService: RegistryLockVerifyService,
|
||||
private route: ActivatedRoute
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.route.queryParamMap.pipe(take(1)).subscribe((params: ParamMap) => {
|
||||
this.registryLockVerifyService
|
||||
.verifyRequest(params.get('lockVerificationCode') || '')
|
||||
.subscribe({
|
||||
error: (err: HttpErrorResponse) => {
|
||||
this.isLoading = false;
|
||||
this.errorMessage = err.error;
|
||||
},
|
||||
next: (verificationResponse) => {
|
||||
this.domainName = verificationResponse.domainName;
|
||||
this.action = verificationResponse.action;
|
||||
this.registrarService.registrarId.set(
|
||||
verificationResponse.registrarId
|
||||
);
|
||||
this.isLoading = false;
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -12,13 +12,20 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component } from '@angular/core';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { BackendService } from '../shared/services/backend.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-users',
|
||||
templateUrl: './users.component.html',
|
||||
styleUrls: ['./users.component.scss'],
|
||||
})
|
||||
export default class UsersComponent {
|
||||
public static PATH = 'users';
|
||||
export interface RegistryLockVerificationResponse {
|
||||
action: string;
|
||||
domainName: string;
|
||||
registrarId: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class RegistryLockVerifyService {
|
||||
constructor(private backendService: BackendService) {}
|
||||
|
||||
verifyRequest(lockVerificationCode: string) {
|
||||
return this.backendService.verifyRegistryLockRequest(lockVerificationCode);
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,11 @@
|
||||
<mat-tree-node
|
||||
*matTreeNodeDef="let node"
|
||||
matTreeNodeToggle
|
||||
tabindex="0"
|
||||
(click)="onClick(node)"
|
||||
[class.active]="router.url.endsWith(node.path)"
|
||||
(keyup.enter)="onClick(node)"
|
||||
[class.active]="router.url.includes(node.path)"
|
||||
[elementId]="getElementId(node)"
|
||||
>
|
||||
<mat-icon class="console-app__nav-icon" *ngIf="node.iconName">
|
||||
{{ node.iconName }}
|
||||
@@ -17,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
|
||||
|
||||
@@ -44,10 +44,10 @@ $expand-icon-size: 26px;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--light-highlight);
|
||||
border-radius: 0 25px 25px 0;
|
||||
border-radius: 0 15px 15px 0;
|
||||
}
|
||||
&.active {
|
||||
border-radius: 0 25px 25px 0;
|
||||
border-radius: 0 15px 15px 0;
|
||||
background-color: var(--lightest);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,9 @@ import { Component } from '@angular/core';
|
||||
import { MatTreeNestedDataSource } from '@angular/material/tree';
|
||||
import { NavigationEnd, Router } from '@angular/router';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { RouteWithIcon, routes } from '../app-routing.module';
|
||||
import { RouteWithIcon, routes, PATHS } from '../app-routing.module';
|
||||
import { RESTRICTED_ELEMENTS } from '../shared/directives/userLevelVisiblity.directive';
|
||||
import { RegistrarComponent } from '../registrar/registrarsTable.component';
|
||||
|
||||
interface NavMenuNode extends RouteWithIcon {
|
||||
parentRoute?: RouteWithIcon;
|
||||
@@ -31,12 +33,14 @@ interface NavMenuNode extends RouteWithIcon {
|
||||
selector: 'app-navigation',
|
||||
templateUrl: './navigation.component.html',
|
||||
styleUrls: ['./navigation.component.scss'],
|
||||
standalone: false,
|
||||
})
|
||||
export class NavigationComponent {
|
||||
renderRouter: boolean = true;
|
||||
treeControl = new NestedTreeControl<RouteWithIcon>((node) => node.children);
|
||||
dataSource = new MatTreeNestedDataSource<RouteWithIcon>();
|
||||
private subscription!: Subscription;
|
||||
|
||||
hasChild = (_: number, node: RouteWithIcon) =>
|
||||
!!node.children && node.children.length > 0;
|
||||
|
||||
@@ -53,7 +57,16 @@ export class NavigationComponent {
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.subscription.unsubscribe();
|
||||
this.subscription && this.subscription.unsubscribe();
|
||||
}
|
||||
|
||||
getElementId(node: RouteWithIcon) {
|
||||
if (node.path === RegistrarComponent.PATH) {
|
||||
return RESTRICTED_ELEMENTS.REGISTRAR_ELEMENT;
|
||||
} else if (node.path === PATHS.UsersComponent) {
|
||||
return RESTRICTED_ELEMENTS.USERS;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
syncExpandedNavigationWithRoute(url: string) {
|
||||
|
||||
43
console-webapp/src/app/ote/newOte.component.html
Normal file
43
console-webapp/src/app/ote/newOte.component.html
Normal file
@@ -0,0 +1,43 @@
|
||||
<h1 class="mat-headline-4">Generate OT&E Accounts</h1>
|
||||
<div class="console-app__new-ote">
|
||||
@if (oteCreateResponseFormatted()) {
|
||||
<h1>Generated Successfully</h1>
|
||||
<mat-card appearance="outlined">
|
||||
<mat-card-header>
|
||||
<mat-card-title>Epp Credentials</mat-card-title>
|
||||
<mat-card-subtitle
|
||||
>Copy and paste this into an email to the registrars</mat-card-subtitle
|
||||
>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<p>{{ oteCreateResponseFormatted() }}</p>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
} @else {
|
||||
<form (ngSubmit)="onSubmit()" [formGroup]="createOte">
|
||||
<p>
|
||||
<mat-form-field name="registrarId" appearance="outline">
|
||||
<mat-label>Base Registrar Id: </mat-label>
|
||||
<input matInput type="text" formControlName="registrarId" required />
|
||||
</mat-form-field>
|
||||
</p>
|
||||
<p>
|
||||
<mat-form-field name="registrarEmail" appearance="outline">
|
||||
<mat-label>Contact Email: </mat-label>
|
||||
<input matInput type="text" formControlName="registrarEmail" required />
|
||||
<mat-hint
|
||||
>Will be granted web-console access to the OTE registrars.</mat-hint
|
||||
>
|
||||
</mat-form-field>
|
||||
</p>
|
||||
<button
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
type="submit"
|
||||
aria-label="Submit new OT&E account"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</form>
|
||||
}
|
||||
</div>
|
||||
11
console-webapp/src/app/ote/newOte.component.scss
Normal file
11
console-webapp/src/app/ote/newOte.component.scss
Normal file
@@ -0,0 +1,11 @@
|
||||
.console-app__new-ote {
|
||||
max-width: 720px;
|
||||
mat-card-content {
|
||||
white-space: break-spaces;
|
||||
padding: 20px;
|
||||
}
|
||||
mat-form-field {
|
||||
width: 100%;
|
||||
max-width: 350px;
|
||||
}
|
||||
}
|
||||
82
console-webapp/src/app/ote/newOte.component.ts
Normal file
82
console-webapp/src/app/ote/newOte.component.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { Component, computed, signal } from '@angular/core';
|
||||
import { FormControl, FormGroup, Validators } from '@angular/forms';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { RegistrarService } from '../registrar/registrar.service';
|
||||
import { MaterialModule } from '../material.module';
|
||||
import { SnackBarModule } from '../snackbar.module';
|
||||
|
||||
export interface OteCreateResponse extends Map<string, string> {
|
||||
password: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-ote',
|
||||
imports: [MaterialModule, SnackBarModule],
|
||||
templateUrl: './newOte.component.html',
|
||||
styleUrls: ['./newOte.component.scss'],
|
||||
})
|
||||
export class NewOteComponent {
|
||||
oteCreateResponse = signal<OteCreateResponse | undefined>(undefined);
|
||||
|
||||
readonly oteCreateResponseFormatted = computed(() => {
|
||||
const oteCreateResponse = this.oteCreateResponse();
|
||||
if (oteCreateResponse) {
|
||||
const { password } = oteCreateResponse;
|
||||
return Object.entries(oteCreateResponse)
|
||||
.filter((entry) => entry[0] !== 'password')
|
||||
.map(
|
||||
([login, tld]) =>
|
||||
`Login: ${login}\t\tPassword: ${password}\t\tTLD: ${tld}`
|
||||
)
|
||||
.join('\n');
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
createOte = new FormGroup({
|
||||
registrarId: new FormControl('', [Validators.required]),
|
||||
registrarEmail: new FormControl('', [Validators.required]),
|
||||
});
|
||||
|
||||
constructor(
|
||||
protected registrarService: RegistrarService,
|
||||
private _snackBar: MatSnackBar
|
||||
) {}
|
||||
|
||||
onSubmit() {
|
||||
if (this.createOte.valid) {
|
||||
const { registrarId, registrarEmail } = this.createOte.value;
|
||||
this.registrarService
|
||||
.generateOte(
|
||||
{
|
||||
registrarId,
|
||||
registrarEmail,
|
||||
},
|
||||
registrarId || ''
|
||||
)
|
||||
.subscribe({
|
||||
next: (oteCreateResponse: OteCreateResponse) => {
|
||||
this.oteCreateResponse.set(oteCreateResponse);
|
||||
},
|
||||
error: (err: HttpErrorResponse) => {
|
||||
this._snackBar.open(err.error || err.message);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
28
console-webapp/src/app/ote/oteStatus.component.html
Normal file
28
console-webapp/src/app/ote/oteStatus.component.html
Normal file
@@ -0,0 +1,28 @@
|
||||
<h1 class="mat-headline-4">OT&E Status Check</h1>
|
||||
@if(registrarId() === null) {
|
||||
<h1>Missing registrarId param</h1>
|
||||
} @else if(isOte()) {
|
||||
<h1 *ngIf="oteStatusResponse().length">
|
||||
Status:
|
||||
<span>{{ oteStatusUnfinished().length ? "Unfinished" : "Completed" }}</span>
|
||||
</h1>
|
||||
<div class="console-app__ote-status">
|
||||
@if(oteStatusCompleted().length) {
|
||||
<div class="console-app__ote-status_completed">
|
||||
<h1>Completed</h1>
|
||||
<div *ngFor="let entry of oteStatusCompleted()">
|
||||
<mat-icon>check_box</mat-icon>{{ entry.description }}
|
||||
</div>
|
||||
</div>
|
||||
} @if(oteStatusUnfinished().length) {
|
||||
<div class="console-app__ote-status_unfinished">
|
||||
<h1>Unfinished</h1>
|
||||
<div *ngFor="let entry of oteStatusUnfinished()">
|
||||
<mat-icon>check_box_outline_blank</mat-icon>{{ entry.description }}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
} @else {
|
||||
<h1>Registrar {{ registrarId() }} is not an OT&E registrar</h1>
|
||||
}
|
||||
28
console-webapp/src/app/ote/oteStatus.component.scss
Normal file
28
console-webapp/src/app/ote/oteStatus.component.scss
Normal file
@@ -0,0 +1,28 @@
|
||||
.console-app__ote-status {
|
||||
max-width: 730px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
&_completed,
|
||||
&_unfinished {
|
||||
border: 1px solid #ddd;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
margin: 0 20px 30px 0;
|
||||
div {
|
||||
display: flex;
|
||||
min-width: 300px;
|
||||
align-items: flex-start;
|
||||
max-width: 300px;
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 5px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
&:last-child {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
mat-icon {
|
||||
min-width: 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
78
console-webapp/src/app/ote/oteStatus.component.ts
Normal file
78
console-webapp/src/app/ote/oteStatus.component.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { Component, computed, OnInit, signal } from '@angular/core';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { RegistrarService } from '../registrar/registrar.service';
|
||||
import { MaterialModule } from '../material.module';
|
||||
import { SnackBarModule } from '../snackbar.module';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
||||
import { take } from 'rxjs';
|
||||
|
||||
export interface OteStatusResponse {
|
||||
description: string;
|
||||
requirement: number;
|
||||
timesPerformed: number;
|
||||
completed: boolean;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-ote-status',
|
||||
imports: [MaterialModule, SnackBarModule, CommonModule],
|
||||
templateUrl: './oteStatus.component.html',
|
||||
styleUrls: ['./oteStatus.component.scss'],
|
||||
})
|
||||
export class OteStatusComponent implements OnInit {
|
||||
registrarId = signal<string | null>(null);
|
||||
oteStatusResponse = signal<OteStatusResponse[]>([]);
|
||||
|
||||
oteStatusCompleted = computed(() =>
|
||||
this.oteStatusResponse().filter((v) => v.completed)
|
||||
);
|
||||
oteStatusUnfinished = computed(() =>
|
||||
this.oteStatusResponse().filter((v) => !v.completed)
|
||||
);
|
||||
isOte = computed(
|
||||
() =>
|
||||
this.registrarService
|
||||
.registrars()
|
||||
.find((r) => r.registrarId === this.registrarId())
|
||||
?.type?.toLowerCase() === 'ote'
|
||||
);
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
protected registrarService: RegistrarService,
|
||||
private _snackBar: MatSnackBar
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.route.paramMap.pipe(take(1)).subscribe((params: ParamMap) => {
|
||||
this.registrarId.set(params.get('registrarId'));
|
||||
const registrarId = this.registrarId();
|
||||
if (!registrarId) throw 'Missing registrarId param';
|
||||
|
||||
this.registrarService.oteStatus(registrarId).subscribe({
|
||||
next: (oteStatusResponse: OteStatusResponse[]) => {
|
||||
this.oteStatusResponse.set(oteStatusResponse);
|
||||
},
|
||||
error: (err: HttpErrorResponse) => {
|
||||
this._snackBar.open(err.error || err.message);
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
183
console-webapp/src/app/registrar/newRegistrar.component.html
Normal file
183
console-webapp/src/app/registrar/newRegistrar.component.html
Normal file
@@ -0,0 +1,183 @@
|
||||
<div class="console-new-registrar">
|
||||
<button
|
||||
mat-icon-button
|
||||
aria-label="Back to registrars list"
|
||||
(click)="goBack()"
|
||||
>
|
||||
<mat-icon>arrow_back</mat-icon>
|
||||
</button>
|
||||
<div class="spacer"></div>
|
||||
<h1>Create a registrar</h1>
|
||||
<form (ngSubmit)="save($event)" #form>
|
||||
<h2>General</h2>
|
||||
<section>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Registrar Name: </mat-label>
|
||||
<input
|
||||
matInput
|
||||
[required]="true"
|
||||
[(ngModel)]="newRegistrar.registrarName"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</section>
|
||||
<section>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Registrar ID: </mat-label>
|
||||
<input
|
||||
matInput
|
||||
[required]="true"
|
||||
[(ngModel)]="newRegistrar.registrarId"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</section>
|
||||
<section>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Registrar email address: </mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="email"
|
||||
[required]="true"
|
||||
[(ngModel)]="newRegistrar.emailAddress"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</section>
|
||||
<section>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Billing Accounts: </mat-label>
|
||||
<textarea
|
||||
matInput
|
||||
required="true"
|
||||
placeholder="USD=billing-id-for-usd
|
||||
JPY=billing-id-for-yen"
|
||||
[ngModel]="billingAccountMap"
|
||||
(ngModelChange)="onBillingAccountMapChange($event)"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
></textarea>
|
||||
</mat-form-field>
|
||||
</section>
|
||||
<section>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>IANA ID: </mat-label>
|
||||
<input
|
||||
matInput
|
||||
[required]="true"
|
||||
[(ngModel)]="newRegistrar.ianaIdentifier"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</section>
|
||||
<section>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>ICANN referral email: </mat-label>
|
||||
<input
|
||||
matInput
|
||||
[required]="true"
|
||||
type="email"
|
||||
[(ngModel)]="newRegistrar.icannReferralEmail"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</section>
|
||||
<section>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Drive ID: </mat-label>
|
||||
<input
|
||||
matInput
|
||||
[required]="true"
|
||||
[(ngModel)]="newRegistrar.driveFolderId"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</section>
|
||||
<h2>Contact Info</h2>
|
||||
<section>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Street address (Line 1): </mat-label>
|
||||
<input
|
||||
matInput
|
||||
[required]="true"
|
||||
[(ngModel)]="localizedAddressStreet.line1"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</section>
|
||||
<section>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Street address (Line 2)</mat-label>
|
||||
<input
|
||||
matInput
|
||||
[required]="false"
|
||||
[(ngModel)]="localizedAddressStreet.line2"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</section>
|
||||
<section>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Street address (Line 3)</mat-label>
|
||||
<input
|
||||
matInput
|
||||
[required]="false"
|
||||
[(ngModel)]="localizedAddressStreet.line3"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</section>
|
||||
<section>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>City: </mat-label>
|
||||
<input
|
||||
matInput
|
||||
[required]="true"
|
||||
[(ngModel)]="newRegistrar.localizedAddress.city"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</section>
|
||||
<section>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>State/Region: </mat-label>
|
||||
<input
|
||||
matInput
|
||||
[required]="true"
|
||||
[(ngModel)]="newRegistrar.localizedAddress.state"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</section>
|
||||
<section>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>ZIP/Postal Code: </mat-label>
|
||||
<input
|
||||
matInput
|
||||
[required]="true"
|
||||
[(ngModel)]="newRegistrar.localizedAddress.zip"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</section>
|
||||
<section>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Country Code (e.g. US): </mat-label>
|
||||
<input
|
||||
matInput
|
||||
[required]="true"
|
||||
[(ngModel)]="newRegistrar.localizedAddress.countryCode"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</section>
|
||||
<button
|
||||
class="console-new-registrar__submit"
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
type="submit"
|
||||
aria-label="Submit new registrar request"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
20
console-webapp/src/app/registrar/newRegistrar.component.scss
Normal file
20
console-webapp/src/app/registrar/newRegistrar.component.scss
Normal file
@@ -0,0 +1,20 @@
|
||||
.console-new-registrar {
|
||||
max-width: 616px;
|
||||
|
||||
h2 {
|
||||
margin: 40px 0 25px 0 !important;
|
||||
}
|
||||
|
||||
section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
mat-form-field {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__submit {
|
||||
margin: 30px 0;
|
||||
}
|
||||
}
|
||||
99
console-webapp/src/app/registrar/newRegistrar.component.ts
Normal file
99
console-webapp/src/app/registrar/newRegistrar.component.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
// Copyright 2024 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import {
|
||||
Component,
|
||||
ElementRef,
|
||||
ViewChild,
|
||||
ViewEncapsulation,
|
||||
} from '@angular/core';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { Registrar, RegistrarService } from './registrar.service';
|
||||
|
||||
interface LocalizedAddressStreet {
|
||||
line1: string;
|
||||
line2: string;
|
||||
line3: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-new-registrar',
|
||||
templateUrl: './newRegistrar.component.html',
|
||||
styleUrls: ['./newRegistrar.component.scss'],
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
standalone: false,
|
||||
})
|
||||
export default class NewRegistrarComponent {
|
||||
protected newRegistrar: Registrar;
|
||||
protected localizedAddressStreet: LocalizedAddressStreet;
|
||||
protected billingAccountMap: String = '';
|
||||
|
||||
@ViewChild('form') form!: ElementRef;
|
||||
constructor(
|
||||
private registrarService: RegistrarService,
|
||||
private _snackBar: MatSnackBar
|
||||
) {
|
||||
this.newRegistrar = {
|
||||
registrarId: '',
|
||||
url: '',
|
||||
registrarName: '',
|
||||
icannReferralEmail: '',
|
||||
localizedAddress: {
|
||||
city: '',
|
||||
state: '',
|
||||
zip: '',
|
||||
countryCode: '',
|
||||
},
|
||||
};
|
||||
this.localizedAddressStreet = {
|
||||
line1: '',
|
||||
line2: '',
|
||||
line3: '',
|
||||
};
|
||||
}
|
||||
|
||||
onBillingAccountMapChange(val: String) {
|
||||
const billingAccountMap: { [key: string]: string } = {};
|
||||
this.newRegistrar.billingAccountMap = val.split('\n').reduce((acc, val) => {
|
||||
const [currency, billingCode] = val.split('=');
|
||||
acc[currency] = billingCode;
|
||||
return acc;
|
||||
}, billingAccountMap);
|
||||
}
|
||||
|
||||
save(e: SubmitEvent) {
|
||||
e.preventDefault();
|
||||
if (this.form.nativeElement.checkValidity()) {
|
||||
const { line1, line2, line3 } = this.localizedAddressStreet;
|
||||
this.newRegistrar.localizedAddress.street = [line1, line2, line3].filter(
|
||||
(v) => !!v
|
||||
);
|
||||
this.registrarService.createRegistrar(this.newRegistrar).subscribe({
|
||||
complete: () => {
|
||||
this.goBack();
|
||||
},
|
||||
error: (err: HttpErrorResponse) => {
|
||||
this._snackBar.open(err.error);
|
||||
},
|
||||
});
|
||||
} else {
|
||||
this.form.nativeElement.reportValidity();
|
||||
}
|
||||
}
|
||||
|
||||
goBack() {
|
||||
this.registrarService.inNewRegistrarMode.set(false);
|
||||
}
|
||||
}
|
||||
@@ -14,18 +14,24 @@
|
||||
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { RegistrarService } from './registrar.service';
|
||||
import { BackendService } from '../shared/services/backend.service';
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
import { provideHttpClientTesting } from '@angular/common/http/testing';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { BackendService } from '../shared/services/backend.service';
|
||||
import { RegistrarService } from './registrar.service';
|
||||
|
||||
describe('RegistrarService', () => {
|
||||
let service: RegistrarService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [HttpClientTestingModule],
|
||||
providers: [BackendService, MatSnackBar],
|
||||
imports: [],
|
||||
providers: [
|
||||
BackendService,
|
||||
MatSnackBar,
|
||||
provideHttpClient(),
|
||||
provideHttpClientTesting(),
|
||||
],
|
||||
});
|
||||
service = TestBed.inject(RegistrarService);
|
||||
});
|
||||
|
||||
@@ -13,10 +13,12 @@
|
||||
// limitations under the License.
|
||||
|
||||
import { Injectable, computed, signal } from '@angular/core';
|
||||
import { Observable, tap } from 'rxjs';
|
||||
import { Observable, switchMap, tap } from 'rxjs';
|
||||
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { Router } from '@angular/router';
|
||||
import { OteCreateResponse } from '../ote/newOte.component';
|
||||
import { OteStatusResponse } from '../ote/oteStatus.component';
|
||||
import { BackendService } from '../shared/services/backend.service';
|
||||
import {
|
||||
GlobalLoader,
|
||||
@@ -48,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;
|
||||
@@ -69,6 +70,8 @@ export interface Registrar
|
||||
registrarId: string;
|
||||
registrarName: string;
|
||||
registryLockAllowed?: boolean;
|
||||
type?: string;
|
||||
lastPocVerificationDate?: string;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
@@ -85,14 +88,21 @@ export class RegistrarService implements GlobalLoader {
|
||||
this.registrars().find((r) => r.registrarId === this.registrarId())
|
||||
);
|
||||
|
||||
inNewRegistrarMode = signal(false);
|
||||
|
||||
registrarsLoaded: Promise<void>;
|
||||
|
||||
constructor(
|
||||
private backend: BackendService,
|
||||
private globalLoader: GlobalLoaderService,
|
||||
private _snackBar: MatSnackBar,
|
||||
private router: Router
|
||||
) {
|
||||
this.loadRegistrars().subscribe((r) => {
|
||||
this.globalLoader.stopGlobalLoader(this);
|
||||
this.registrarsLoaded = new Promise((resolve) => {
|
||||
this.loadRegistrars().subscribe((r) => {
|
||||
this.globalLoader.stopGlobalLoader(this);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
this.globalLoader.startGlobalLoader(this);
|
||||
}
|
||||
@@ -118,19 +128,23 @@ export class RegistrarService implements GlobalLoader {
|
||||
);
|
||||
}
|
||||
|
||||
saveRegistrar(registrar: Registrar) {
|
||||
return this.backend.postRegistrar(registrar).pipe(
|
||||
tap((registrar) => {
|
||||
if (registrar) {
|
||||
this.registrars.set(
|
||||
this.registrars().map((r) => {
|
||||
if (r.registrarId === registrar.registrarId) {
|
||||
return registrar;
|
||||
}
|
||||
return r;
|
||||
})
|
||||
);
|
||||
}
|
||||
createRegistrar(registrar: Registrar) {
|
||||
return this.backend
|
||||
.createRegistrar(registrar)
|
||||
.pipe(switchMap((_) => this.loadRegistrars()));
|
||||
}
|
||||
|
||||
updateRegistrar(updatedRegistrar: Registrar) {
|
||||
return this.backend.updateRegistrar(updatedRegistrar).pipe(
|
||||
tap(() => {
|
||||
this.registrars.set(
|
||||
this.registrars().map((r) => {
|
||||
if (r.registrarId === updatedRegistrar.registrarId) {
|
||||
return updatedRegistrar;
|
||||
}
|
||||
return r;
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -138,4 +152,17 @@ export class RegistrarService implements GlobalLoader {
|
||||
loadingTimeout() {
|
||||
this._snackBar.open('Timeout loading registrars');
|
||||
}
|
||||
|
||||
generateOte(
|
||||
oteForm: Object,
|
||||
registrarId: string
|
||||
): Observable<OteCreateResponse> {
|
||||
return this.backend
|
||||
.generateOte(oteForm, registrarId)
|
||||
.pipe(tap((_) => this.loadRegistrars()));
|
||||
}
|
||||
|
||||
oteStatus(registrarId: string): Observable<OteStatusResponse[]> {
|
||||
return this.backend.getOteStatus(registrarId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
@@ -8,6 +12,15 @@
|
||||
</button>
|
||||
<div class="spacer"></div>
|
||||
@if(!inEdit && !registrarNotFound) {
|
||||
<button
|
||||
*ngIf="oteButtonVisible"
|
||||
mat-stroked-button
|
||||
(click)="checkOteStatus()"
|
||||
aria-label="Check OT&E account status"
|
||||
[elementId]="getElementIdForOteBlock()"
|
||||
>
|
||||
Check OT&E Status
|
||||
</button>
|
||||
<button
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
|
||||
@@ -18,19 +18,23 @@ import { MatChipInputEvent } from '@angular/material/chips';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { RESTRICTED_ELEMENTS } from '../shared/directives/userLevelVisiblity.directive';
|
||||
import { Registrar, RegistrarService } from './registrar.service';
|
||||
import { RegistrarComponent, columns } from './registrarsTable.component';
|
||||
import { environment } from '../../environments/environment';
|
||||
|
||||
@Component({
|
||||
selector: 'app-registrar-details',
|
||||
templateUrl: './registrarDetails.component.html',
|
||||
styleUrls: ['./registrarDetails.component.scss'],
|
||||
standalone: false,
|
||||
})
|
||||
export class RegistrarDetailsComponent implements OnInit {
|
||||
public static PATH = 'registrars/:id';
|
||||
inEdit: boolean = false;
|
||||
oteButtonVisible = environment.sandbox;
|
||||
registrarInEdit!: Registrar;
|
||||
registrarNotFound: boolean = false;
|
||||
registrarNotFound: boolean = true;
|
||||
columns = columns.filter((c) => !c.hiddenOnDetailsCard);
|
||||
private subscription!: Subscription;
|
||||
|
||||
@@ -42,28 +46,42 @@ export class RegistrarDetailsComponent implements OnInit {
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.subscription = this.route.paramMap.subscribe((params: ParamMap) => {
|
||||
this.registrarInEdit = structuredClone(
|
||||
this.registrarService
|
||||
.registrars()
|
||||
.filter((r) => r.registrarId === params.get('id'))[0]
|
||||
);
|
||||
if (!this.registrarInEdit) {
|
||||
this._snackBar.open(
|
||||
`Registrar with id ${params.get('id')} is not available`
|
||||
this.registrarService.registrarsLoaded.then(() => {
|
||||
this.subscription = this.route.paramMap.subscribe((params: ParamMap) => {
|
||||
this.registrarInEdit = structuredClone(
|
||||
this.registrarService
|
||||
.registrars()
|
||||
.filter((r) => r.registrarId === params.get('id'))[0]
|
||||
);
|
||||
this.registrarNotFound = true;
|
||||
} else {
|
||||
this.registrarNotFound = false;
|
||||
}
|
||||
if (!this.registrarInEdit) {
|
||||
this._snackBar.open(
|
||||
`Registrar with id ${params.get('id')} is not available`
|
||||
);
|
||||
this.registrarNotFound = true;
|
||||
} else {
|
||||
this.registrarNotFound = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
addTLD(e: MatChipInputEvent) {
|
||||
this.registrarInEdit.allowedTlds = this.registrarInEdit.allowedTlds || [];
|
||||
this.removeTLD(e.value); // Prevent dups
|
||||
this.registrarInEdit.allowedTlds = this.registrarInEdit.allowedTlds?.concat(
|
||||
[e.value.toLowerCase()]
|
||||
);
|
||||
this.registrarInEdit.allowedTlds = [
|
||||
...this.registrarInEdit.allowedTlds,
|
||||
e.value.toLowerCase(),
|
||||
];
|
||||
}
|
||||
|
||||
checkOteStatus() {
|
||||
this.router.navigate(['ote-status/', this.registrarInEdit.registrarId], {
|
||||
queryParamsHandling: 'merge',
|
||||
});
|
||||
}
|
||||
|
||||
getElementIdForOteBlock() {
|
||||
return RESTRICTED_ELEMENTS.OTE;
|
||||
}
|
||||
|
||||
removeTLD(tld: string) {
|
||||
@@ -73,7 +91,7 @@ export class RegistrarDetailsComponent implements OnInit {
|
||||
}
|
||||
|
||||
saveAndClose() {
|
||||
this.registrarService.saveRegistrar(this.registrarInEdit).subscribe({
|
||||
this.registrarService.updateRegistrar(this.registrarInEdit).subscribe({
|
||||
complete: () => {
|
||||
this.router.navigate([RegistrarComponent.PATH], {
|
||||
queryParamsHandling: 'merge',
|
||||
@@ -87,6 +105,6 @@ export class RegistrarDetailsComponent implements OnInit {
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.subscription.unsubscribe();
|
||||
this.subscription && this.subscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
<div class="console-app__registrar">
|
||||
<mat-form-field
|
||||
class="example-full-width"
|
||||
class="mat-form-field-density-5"
|
||||
appearance="outline"
|
||||
>
|
||||
<mat-form-field class="field-small" appearance="outline">
|
||||
<mat-label>Registrar</mat-label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -15,6 +11,7 @@
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
(focus)="onFocus()"
|
||||
[matAutocomplete]="auto"
|
||||
spellcheck="false"
|
||||
/>
|
||||
<mat-autocomplete
|
||||
autoActiveFirstOption
|
||||
|
||||
@@ -14,11 +14,13 @@
|
||||
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { RegistrarSelectorComponent } from './registrarSelector.component';
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
import { provideHttpClientTesting } from '@angular/common/http/testing';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { MaterialModule } from '../material.module';
|
||||
import { BackendService } from '../shared/services/backend.service';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { RegistrarSelectorComponent } from './registrarSelector.component';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
|
||||
describe('RegistrarSelectorComponent', () => {
|
||||
let component: RegistrarSelectorComponent;
|
||||
@@ -26,13 +28,13 @@ describe('RegistrarSelectorComponent', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
MaterialModule,
|
||||
BrowserAnimationsModule,
|
||||
],
|
||||
providers: [BackendService],
|
||||
declarations: [RegistrarSelectorComponent],
|
||||
imports: [MaterialModule, BrowserAnimationsModule, FormsModule],
|
||||
providers: [
|
||||
BackendService,
|
||||
provideHttpClient(),
|
||||
provideHttpClientTesting(),
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(RegistrarSelectorComponent);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -1,38 +1,76 @@
|
||||
@if(registrarService.inNewRegistrarMode()) {
|
||||
<app-new-registrar />
|
||||
} @else {
|
||||
<div class="console-app__registrars">
|
||||
<h1 class="mat-headline-4">Registrars</h1>
|
||||
<mat-form-field class="console-app__registrars-filter">
|
||||
<mat-label>Search</mat-label>
|
||||
<input
|
||||
matInput
|
||||
(keyup)="applyFilter($event)"
|
||||
placeholder="..."
|
||||
type="search"
|
||||
/>
|
||||
<mat-icon matPrefix>search</mat-icon>
|
||||
</mat-form-field>
|
||||
<mat-table
|
||||
[dataSource]="dataSource"
|
||||
class="mat-elevation-z0"
|
||||
class="console-app__registrars-table"
|
||||
matSort
|
||||
>
|
||||
<ng-container
|
||||
*ngFor="let column of columns"
|
||||
[matColumnDef]="column.columnDef"
|
||||
<div class="console-app__registrars-header">
|
||||
<h1 class="mat-headline-4" forceFocus>Registrars</h1>
|
||||
<div class="spacer"></div>
|
||||
<button
|
||||
mat-stroked-button
|
||||
*ngIf="oteButtonVisible"
|
||||
(click)="createOteAccount()"
|
||||
aria-label="Generate OT&E accounts"
|
||||
[elementId]="getElementIdForOteBlock()"
|
||||
>
|
||||
<mat-header-cell *matHeaderCellDef> {{ column.header }} </mat-header-cell>
|
||||
<mat-cell *matCellDef="let row" [innerHTML]="column.cell(row)"></mat-cell>
|
||||
</ng-container>
|
||||
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
|
||||
<mat-row
|
||||
*matRowDef="let row; columns: displayedColumns"
|
||||
(click)="openDetails(row.registrarId)"
|
||||
></mat-row>
|
||||
</mat-table>
|
||||
Create OT&E accounts
|
||||
</button>
|
||||
<button
|
||||
class="console-app__registrars-new"
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
(click)="openNewRegistrar()"
|
||||
aria-label="Add new registrar"
|
||||
>
|
||||
<mat-icon>add</mat-icon>
|
||||
Add new registrar
|
||||
</button>
|
||||
</div>
|
||||
<div class="console-app__scrollable-wrapper">
|
||||
<div class="console-app__scrollable">
|
||||
<mat-form-field class="console-app__registrars-filter">
|
||||
<mat-label>Search</mat-label>
|
||||
<input
|
||||
matInput
|
||||
(keyup)="applyFilter($event)"
|
||||
placeholder="..."
|
||||
type="search"
|
||||
/>
|
||||
<mat-icon matPrefix>search</mat-icon>
|
||||
</mat-form-field>
|
||||
<mat-table
|
||||
[dataSource]="dataSource"
|
||||
class="mat-elevation-z0"
|
||||
class="console-app__registrars-table"
|
||||
matSort
|
||||
>
|
||||
<ng-container
|
||||
*ngFor="let column of columns"
|
||||
[matColumnDef]="column.columnDef"
|
||||
>
|
||||
<mat-header-cell *matHeaderCellDef>
|
||||
{{ column.header }}
|
||||
</mat-header-cell>
|
||||
<mat-cell
|
||||
*matCellDef="let row"
|
||||
[innerHTML]="column.cell(row)"
|
||||
></mat-cell>
|
||||
</ng-container>
|
||||
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
|
||||
<mat-row
|
||||
*matRowDef="let row; columns: displayedColumns"
|
||||
(click)="openDetails(row.registrarId)"
|
||||
tabindex="0"
|
||||
(keyup.enter)="openDetails(row.registrarId)"
|
||||
></mat-row>
|
||||
</mat-table>
|
||||
|
||||
<mat-paginator
|
||||
class="mat-elevation-z0"
|
||||
[pageSizeOptions]="[5, 10, 20]"
|
||||
showFirstLastButtons
|
||||
></mat-paginator>
|
||||
<mat-paginator
|
||||
class="mat-elevation-z0"
|
||||
[pageSizeOptions]="[5, 10, 20]"
|
||||
showFirstLastButtons
|
||||
></mat-paginator>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
}
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
.console-app {
|
||||
$min-width: 756px;
|
||||
|
||||
&__registrars {
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
&__registrars-filter {
|
||||
min-width: $min-width !important;
|
||||
width: 100%;
|
||||
@@ -15,6 +10,15 @@
|
||||
min-width: $min-width !important;
|
||||
}
|
||||
|
||||
&__registrars-new {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
&__registrars-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.mat-mdc-paginator {
|
||||
min-width: $min-width !important;
|
||||
}
|
||||
@@ -26,7 +30,7 @@
|
||||
}
|
||||
&-driveId {
|
||||
min-width: 200px;
|
||||
word-break: break-all;
|
||||
word-break: break-word;
|
||||
}
|
||||
&-registryLockAllowed {
|
||||
max-width: 80px;
|
||||
|
||||
@@ -14,12 +14,13 @@
|
||||
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { RegistrarComponent } from './registrarsTable.component';
|
||||
import { BackendService } from '../shared/services/backend.service';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
import { provideHttpClientTesting } from '@angular/common/http/testing';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { MaterialModule } from '../material.module';
|
||||
import { BackendService } from '../shared/services/backend.service';
|
||||
import { RegistrarComponent } from './registrarsTable.component';
|
||||
|
||||
describe('RegistrarComponent', () => {
|
||||
let component: RegistrarComponent;
|
||||
@@ -28,14 +29,12 @@ describe('RegistrarComponent', () => {
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [RegistrarComponent],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
MaterialModule,
|
||||
BrowserAnimationsModule,
|
||||
],
|
||||
imports: [MaterialModule, BrowserAnimationsModule],
|
||||
providers: [
|
||||
BackendService,
|
||||
{ provide: ActivatedRoute, useValue: {} as ActivatedRoute },
|
||||
provideHttpClient(),
|
||||
provideHttpClientTesting(),
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
|
||||
@@ -12,12 +12,15 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, ViewChild, ViewEncapsulation } from '@angular/core';
|
||||
import { Component, effect, ViewChild, ViewEncapsulation } from '@angular/core';
|
||||
import { MatPaginator } from '@angular/material/paginator';
|
||||
import { MatSort } from '@angular/material/sort';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { Router } from '@angular/router';
|
||||
import { RESTRICTED_ELEMENTS } from '../shared/directives/userLevelVisiblity.directive';
|
||||
import { Registrar, RegistrarService } from './registrar.service';
|
||||
import { PATHS } from '../app-routing.module';
|
||||
import { environment } from '../../environments/environment';
|
||||
|
||||
export const columns = [
|
||||
{
|
||||
@@ -51,10 +54,12 @@ export const columns = [
|
||||
columnDef: 'billingAccountMap',
|
||||
header: 'Billing Accounts',
|
||||
cell: (record: Registrar) =>
|
||||
// @ts-ignore - completely legit line, but TS keeps complaining
|
||||
`${Object.entries(record.billingAccountMap).reduce((acc, [key, val]) => {
|
||||
return `${acc}${key}=${val}<br/>`;
|
||||
}, '')}`,
|
||||
`${Object.entries(record.billingAccountMap || {}).reduce(
|
||||
(acc, [key, val]) => {
|
||||
return `${acc}${key}=${val}<br/>`;
|
||||
},
|
||||
''
|
||||
)}`,
|
||||
},
|
||||
{
|
||||
columnDef: 'registryLockAllowed',
|
||||
@@ -73,12 +78,13 @@ export const columns = [
|
||||
templateUrl: './registrarsTable.component.html',
|
||||
styleUrls: ['./registrarsTable.component.scss'],
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
standalone: false,
|
||||
})
|
||||
export class RegistrarComponent {
|
||||
public static PATH = 'registrars';
|
||||
dataSource: MatTableDataSource<Registrar>;
|
||||
columns = columns;
|
||||
|
||||
oteButtonVisible = environment.sandbox;
|
||||
displayedColumns = this.columns.map((c) => c.columnDef);
|
||||
|
||||
@ViewChild(MatPaginator) paginator!: MatPaginator;
|
||||
@@ -91,6 +97,9 @@ export class RegistrarComponent {
|
||||
this.dataSource = new MatTableDataSource<Registrar>(
|
||||
registrarService.registrars()
|
||||
);
|
||||
effect(() => {
|
||||
this.dataSource.data = registrarService.registrars();
|
||||
});
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
@@ -98,6 +107,14 @@ export class RegistrarComponent {
|
||||
this.dataSource.sort = this.sort;
|
||||
}
|
||||
|
||||
createOteAccount() {
|
||||
this.router.navigate([PATHS.NewOteComponent]);
|
||||
}
|
||||
|
||||
getElementIdForOteBlock() {
|
||||
return RESTRICTED_ELEMENTS.OTE;
|
||||
}
|
||||
|
||||
openDetails(registrarId: string) {
|
||||
this.router.navigate(['registrars/', registrarId], {
|
||||
queryParamsHandling: 'merge',
|
||||
@@ -109,4 +126,8 @@ export class RegistrarComponent {
|
||||
// TODO: consider filteing out only by registrar name
|
||||
this.dataSource.filter = filterValue.trim().toLowerCase();
|
||||
}
|
||||
|
||||
openNewRegistrar() {
|
||||
this.registrarService.inNewRegistrarMode.set(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
<h1 class="mat-headline-4">Resource</h1>
|
||||
<h1 class="mat-headline-4" forceFocus>Resources</h1>
|
||||
<div class="console-app__resources">
|
||||
<div>
|
||||
<div class="console-app__resources-subhead">Technical resources</div>
|
||||
<a
|
||||
class="text-l"
|
||||
href="{{ userDataService.userData.technicalDocsUrl }}"
|
||||
href="{{ userDataService.userData()?.technicalDocsUrl }}"
|
||||
target="_blank"
|
||||
>View on Google Drive</a
|
||||
>View onboarding FAQs, TLD information, and technical documentation on
|
||||
Google Drive</a
|
||||
>
|
||||
</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>
|
||||
}
|
||||
|
||||
@@ -14,10 +14,11 @@
|
||||
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import ContactComponent from './contact.component';
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
import { provideHttpClientTesting } from '@angular/common/http/testing';
|
||||
import { MaterialModule } from 'src/app/material.module';
|
||||
import { BackendService } from 'src/app/shared/services/backend.service';
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
import ContactComponent from './contact.component';
|
||||
|
||||
describe('ContactComponent', () => {
|
||||
let component: ContactComponent;
|
||||
@@ -26,8 +27,12 @@ describe('ContactComponent', () => {
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ContactComponent],
|
||||
imports: [HttpClientTestingModule, MaterialModule],
|
||||
providers: [BackendService],
|
||||
imports: [MaterialModule],
|
||||
providers: [
|
||||
BackendService,
|
||||
provideHttpClient(),
|
||||
provideHttpClientTesting(),
|
||||
],
|
||||
}).compileComponents();
|
||||
fixture = TestBed.createComponent(ContactComponent);
|
||||
component = fixture.componentInstance;
|
||||
|
||||
@@ -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';
|
||||
@@ -71,7 +68,7 @@ export default class ContactComponent {
|
||||
effect(() => {
|
||||
if (this.contactService.contacts()) {
|
||||
this.dataSource = new MatTableDataSource<ViewReadyContact>(
|
||||
this.contactService.contacts().map(contactTypeToViewReadyContact)
|
||||
this.contactService.contacts()
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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
|
||||
@@ -18,7 +23,11 @@
|
||||
<mat-icon>edit</mat-icon>
|
||||
Edit
|
||||
</button>
|
||||
<button mat-icon-button aria-label="Delete Contact">
|
||||
<button
|
||||
mat-icon-button
|
||||
aria-label="Delete Contact"
|
||||
(click)="deleteContact()"
|
||||
>
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
}
|
||||
@@ -47,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>
|
||||
|
||||
@@ -74,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>
|
||||
@@ -119,6 +139,7 @@
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
type="submit"
|
||||
aria-label="Save contact updates"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
@@ -166,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>
|
||||
|
||||
@@ -26,6 +26,6 @@
|
||||
mat-form-field {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-bottom: 30px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -50,6 +51,9 @@ export class ContactDetailsComponent {
|
||||
error: (err: HttpErrorResponse) => {
|
||||
this._snackBar.open(err.error);
|
||||
},
|
||||
complete: () => {
|
||||
this.goBack();
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -65,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();
|
||||
@@ -78,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
|
||||
@@ -85,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)
|
||||
@@ -101,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,11 +1,11 @@
|
||||
.console-app__whois {
|
||||
.console-app__rdap {
|
||||
max-width: 616px;
|
||||
|
||||
&-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin: 20px 0;
|
||||
margin-bottom: 20px;
|
||||
button {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@@ -14,32 +14,38 @@
|
||||
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import WhoisComponent from './whois.component';
|
||||
import { MaterialModule } from 'src/app/material.module';
|
||||
import { BackendService } from 'src/app/shared/services/backend.service';
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
import { RegistrarService } from 'src/app/registrar/registrar.service';
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
import { provideHttpClientTesting } from '@angular/common/http/testing';
|
||||
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 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],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
MaterialModule,
|
||||
BrowserAnimationsModule,
|
||||
],
|
||||
declarations: [RdapComponent],
|
||||
imports: [MaterialModule, BrowserAnimationsModule],
|
||||
providers: [
|
||||
BackendService,
|
||||
{ provide: RegistrarService, useValue: { registrar: {} } },
|
||||
{
|
||||
provide: RegistrarService,
|
||||
useValue: {
|
||||
registrar: function () {
|
||||
return {};
|
||||
},
|
||||
},
|
||||
},
|
||||
provideHttpClient(),
|
||||
provideHttpClientTesting(),
|
||||
],
|
||||
}).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();
|
||||
@@ -37,7 +37,7 @@ export default class WhoisComponent {
|
||||
result += `${registrar?.localizedAddress?.state} `;
|
||||
}
|
||||
if (registrar?.localizedAddress?.countryCode) {
|
||||
result += registrar?.localizedAddress?.countryCode;
|
||||
result += `${registrar?.localizedAddress?.countryCode} `;
|
||||
}
|
||||
if (registrar?.localizedAddress?.zip) {
|
||||
result += registrar?.localizedAddress?.zip;
|
||||
@@ -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,29 +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>
|
||||
|
||||
<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>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user