1
0
mirror of https://github.com/google/nomulus synced 2026-01-16 02:33:16 +00:00

Compare commits

...

359 Commits

Author SHA1 Message Date
Ben McIlwain
c187c92ae4 Allow creation of hostnames on .zz-- style TLDs for RST (#2935)
This is a follow-on to PR #2909, which fixed the issue for domains, but
apparently not fully for hostnames.

BUG= http://b/476144993
2026-01-15 20:37:32 +00:00
gbrodman
22ca4e3f2b Disable old fee extensions in non-prod envs (#2933)
The primary annoyance with this is that it means we need (or at least,
should) split all tests that use the fee extension into two separate
tests -- one that simulates non-prod environments, and one that
simulates prod environments. This leads to duplication of many tests but
that's fine since this is theoretically temporary.
2026-01-14 19:04:22 +00:00
Nilay Shah
f27136458a Configure cloud scheduler to trigger MoSAPI SLA status to cloud monitoring (#2926)
* Configure cloud scheduler to trigger MoSAPI SLA status to cloud monitoring in production

- We have kept this job to trigger for every 3 minutes so that we get near to real time update for our task.
- This will not trigger metrics for now as we have not written Metrics triggering logic yet
- Logs are added

* Change Trigger scheduling from 3 minutes to 5 minutes
2026-01-13 18:48:43 +00:00
Ben McIlwain
d8e647316e Remove contact as a supported object type in EPP (#2932)
This primarily affects the EPP greeting. We already were erroring out when any
contact flows attempted to be run; this should just prevent registrars from even
trying them at all.

This PR is designed to be minimally invasive, and does not remove any of the
contact flows or Jakarta XML/XJC objects/files themselves. That can be done
later as a follow-up.

Also note that the contact namespace urn:ietf:params:xml:ns:contact-1.0 is still
present for now in RDE exports, but I'll remove that subsequently as well.

BUG= http://b/475506288
2026-01-13 17:21:03 +00:00
Ben McIlwain
d6e0a7b979 Change domain update commands to be varipotent by status (#2930)
This means that attempting to add a status that is already present will now
fail, and attempting to remove a status that is not present will also now fail.

This also refactors the existing checks into a single verify method, rather than
having to call three separate methods from every callsite.

BUG= http://b/474645068
2026-01-12 22:12:08 +00:00
Juan Celhay
5725eb95e0 Add Cloud java profiler to nomulus docker images (#2919)
* add cloud profiler to dockerfile and start script

* add apt-get update

* change in cb machine type for nomulus

* fix typo

* add max worker limit to gradle tests

* Switch to root before doing apt-get

* correct dockerfile

* jetty/Dockerfile

* profiler service conditional to kubernetes container name
2026-01-12 15:19:05 +00:00
Pavlo Tkach
aa12998276 Increase console workload memory allocation (#2929) 2026-01-09 19:27:07 +00:00
gbrodman
d415416bc5 Update the fee extension 1.0 and add some tests (#2925)
Many of the actual fee extension changes are based off Weimin's PR
https://github.com/google/nomulus/pull/2912, though this makes some
additional changes based on the XML schema and description from RFC 8748.

This adds tests for the DomainCheckFlow which is the most complex and
thorough user of the fee extension, but we'll want to add further tests
to the other domain flows to make sure they're handled correctly.
2026-01-09 18:09:17 +00:00
gbrodman
3a1068f313 Add indexes on current_package_token in Domain* (#2916)
It just makes it possible to delete allocation tokens, otherwise we need
to do a linear search over the entire Domain and DomainHistory tables if
we ever want to delete something.
2026-01-09 17:55:37 +00:00
gbrodman
69e5d40f04 Forbid no-op domain-NS and host-IP adds/removes (#2928)
The RST testing expects us to fail if they try to remove an IP from a
host that already doesn't that have that IP, or to add one that already
exists (ditto on both for a domain's nameservers). I don't really see an
issue with our previous no-op implementation, but we need to do this to
pass the tests.
2026-01-09 17:55:12 +00:00
gbrodman
64f6cd9af4 Only include fee 1.0 extension in nonprod envs (#2927)
We need to have this enabled in sandbox, but we wish to wait to enable
it for production to make sure that the implementation is correct and
that clients can use it.

Soon we'll want to do something similar (but the opposite) with the old
fee extensions, where we **only** serve them in production (or maybe
unit test as well). That will allow us to pass the RST tests that depend
on only having the fee extension 1.0.
2026-01-08 22:00:39 +00:00
gbrodman
40184689ca Allow for a currency unit in fee:check responses (#2922)
This is / will be required in https://datatracker.ietf.org/doc/rfc8748/.
I split this out from the rest of the fee-extension testing so that it
can be easily visible.
2026-01-07 21:12:20 +00:00
Nilay Shah
826ad85d20 Add endpoint to trigger MoSAPI metrics export (#2923)
This commit introduces a new backend endpoint at `/_dr/task/triggerMosApiServiceState` that initiates the process of fetching the latest service states for all TLDs from the MoSAPI endpoint and exporting them as metrics to Cloud Monitoring.

  The key changes include:
   - A new `TriggerServiceStateAction` class that handles the GET request to the new endpoint.
     - Logic within `MosApiStateService` to concurrently fetch states for all configured TLDs.
     - A new `MosApiMetrics` class (currently a placeholder) responsible for sending the collected states to the monitoring service.
     - Unit tests for the new action and the updated service logic.

This endpoint will be called periodically to ensure that the MosApi service health metrics are kept up-to-date.
2026-01-07 19:13:19 +00:00
gbrodman
2b47bc9b0a Move fee class from extension to item (#2924)
this is coming from the schema https://datatracker.ietf.org/doc/rfc8748/
section 6.1. The class, that we use for "premium" notes, moved from the
command to the object itself.
2026-01-06 19:00:19 +00:00
gbrodman
9555dca8c6 Don't allow loopback IP addresses for hosts (#2920)
I don't know where in the spec these are explicitly disallowed, but it
seems like good practice and we'll fail the RST tests if we don't
disallow them.
2026-01-05 21:29:15 +00:00
Ben McIlwain
49484c06d3 Filter out registrars of type OT&E from RDE escrow deposits (#2921)
The RDE XML schema (which is verified by ICANN's RST) requires the presence of a
numeric IANA identifier, which is always null for OT&E registrars. This change
synchronizes the three types of registrars that must have a null IANA identifier
(see
https://cs.opensource.google/nomulus/nomulus/+/master:core/src/main/java/google/registry/model/registrar/Registrar.java;l=109-142;drc=b1266c95e8d9f8206415d2821929d4161869b699
) with the registrars that are excluded from the RDE deposit. Note that there
are no registrars of type OT&E in prod and I can't think of a reason they would
need to be included in escrow deposits on sandbox.
2026-01-05 21:20:11 +00:00
Nilay Shah
81d222e7d6 Add GetServiceState action for MoSAPI service monitoring (#2906)
* Add GetServiceState action for MoSAPI service monitoring

Implements the `/api/mosapi/getServiceState` endpoint to retrieve service health summaries for TLDs from the MoSAPI system.

- Introduces `GetServiceStateAction` to fetch TLD service status.
- Implements `MosApiStateService` to transform raw MoSAPI responses into a curated `ServiceStateSummary`.
- Uses concurrent processing with a fixed thread pool to fetch states for all configured TLDs efficiently while respecting MoSAPI rate limits.

junit test added

* Refactor MoSAPI models to records and address review nits

- Convert model classes to Java records for conciseness and immutability.
- Update unit tests to use Java text blocks for improved JSON readability.
- Simplify service and action layers by removing redundant logic and logging.
- Fix configuration nits regarding primitive types and comment formatting.

* Consolidate MoSAPI models and enhance null-safety

- Moves model records into a single MosApiModels.java file.
- Switches to ImmutableList/ImmutableMap with non-null defaults in constructors.
- Removes redundant pass-through methods in MosApiStateService.
- Updates tests to use Java Text Blocks and non-null collection assertions.

* Improve MoSAPI client error handling and clean up data models

Refactors the MoSAPI monitoring client to be more robust against
infrastructure failures

* Refactor: use nullToEmptyImmutableCopy() for MoSAPI models

Standardize null-handling in model classes by using the Nomulus
`nullToEmptyImmutableCopy()` utility. This ensures consistent API
responses with empty lists instead of omitted fields.
2026-01-05 15:44:01 +00:00
Weimin Yu
7e9d4c27d1 Use downloaded Gradle distribution on Cloud Build (#2918)
This way we get around the http url and no longer needs public access on
the GCS bucket.
2025-12-30 21:08:04 +00:00
Weimin Yu
f9c22ff1c5 Add RST support in Sandbox (#2917)
* Add RST support in Sandbox

Added RST test label files as resources.

Added a RstTmchUtils class that loads appropriate labels according to
TLD pattern.

Temporarily changed label fetching in production to include the TLD
string, so that the new class may know which set of labels to use.

* Addressing comments

* Addressing comments
2025-12-30 20:59:28 +00:00
gbrodman
2562d582f3 Add more strict hostname validation on host:check flows (#2915)
We do most of these on host create already so we should also do them on
host checks. The only added change is the character validation (our
existing hostnames all match these).
2025-12-30 16:41:56 +00:00
Ben McIlwain
6f0bc1ded9 Add Augmented Latin IDN table to IDN enums (#2914)
This was added in https://github.com/google/nomulus/pull/2884 , but now as of
this PR it can actually be configured and used on a TLD.
2025-12-27 00:57:24 +00:00
gbrodman
db9fc3271d Change EPP errors 2306->2005 for some structural issues (#2911)
2306 signifies something that is syntactically valid but semantically
invalid (like if someone tried to register a .com domain). These errors
are for domain syntax that could never be valid, thus we should throw a
syntax exception instead of a policy exception.
2025-12-26 16:08:04 +00:00
Ben McIlwain
84491fde70 Don't allow underscores in TLD ROID suffixes (#2913)
Per ICANN it's a disallowed character.
2025-12-26 16:01:28 +00:00
Juan Celhay
0519e2ffcf Change gradle memory/workers to avoid OOM in CB (#2910) 2025-12-23 15:49:25 +00:00
gbrodman
85f75494ab Remove implementation of contact flows (#2896)
Now that we have transitioned to the minimum dataset, we no longer
support any actions on contacts (and by the time this is merged /
deployed, all contacts will be deleted). We should just throw an
appropriate exception on all contact-related flows. We don't delete the
flows themselves, so that we can have an appropriate error message.

We also keep all the flows and XML templates around individually for now because we may be
required to continue to differentiate the requests in ICANN activity
reporting (e.g. srs-cont-create vs srs-cont-delete)
2025-12-23 15:38:24 +00:00
Ben McIlwain
cbba91558a Allow double hyphens in 3rd&4th position in all domain operations (#2909)
This is a follow-up to PR #2908, which relaxed this restriction on bare TLDs
only, but now we also allow it systemwide on domains and hostnames as well.  The
rules against hyphens in these positions are still enforced on all parts of the
domain name except the last one. Correct handling of multi-part TLDs in this
regard is out of scope in this PR; a multi-part TLD that looked something like
".zz--foobar.foobar" would still fail validation. (But of course you cannot a
priori know just from looking at a 3-part string whether it might be a hostname
on a normal TLD, or a domain name on a 2-part TLD.)

This also has some annoying interactions with a trailing dot (indicating the
root), which need to be preserved, but otherwise don't affect how TLD validation
is handled.

BUG= http://b/471013082
2025-12-23 00:57:57 +00:00
Ben McIlwain
c24f09febc Don't call canonicalizeHostname() on nomulus command TLD args (#2908)
The canonicalizeHostname() helper method is only suitable for use with domain
names or host names. It does not work on bare TLDs, because a bare TLD can
have hyphens in the third and fourth position without necessarily being an IDN.
Note that the configure TLD command already correctly allows TLDs with such
names to be created.

Note that we are still enforcing that the TLDs to be added exist, so they have
to pass all TLD naming requirements that are enforced on creating TLDs, and we
are still lowercasing the TLD names passed as arguments here (though we're no
longer punycoding them, although arguably that's not super useful on
command-line params anyway).

BUG= http://b/471013082
2025-12-22 21:34:55 +00:00
Weimin Yu
fd51035f23 Stop depending on GCS public access for Kokoro (#2907)
We used to publish test artifacts to a Maven repo on GCS, for use by
schema tests. For this to work with Kokoro, the GCS bucket must be
accessible to all users.

To comply with the no-public-user requirement, we store the necessary
jars at at well-known bucket and map them into Kokoro. This strategy
cannot be used on the Maven repo because only a small number of files
with fixed names may be mapped. With the Maven repo, there are too many
files to map.
2025-12-17 20:55:03 +00:00
gbrodman
90eb078e3f Add a BulkDomainTransferCommand (#2898)
This is a decently simple wrapper around the previously-created
BulkDomainTransferAction that batches a provided list of domains up and
sends them along to be transferred.
2025-12-12 21:15:47 +00:00
gbrodman
2a94bdc257 Add a command to delete feature flags (#2904)
This allows us to delete old ones to avoid confusion, and so that we can
more easily clean up the codebase.
2025-12-11 21:52:59 +00:00
gbrodman
50fa49e0c0 Always act as if contacts are prohibited (#2897)
This PR finds instances where we previously checked if the feature flag
for contacts-prohibited was set and removes those checks, making the
contacts-prohibited behavior the only behavior. Because the tests didn't
have that feature flag set, this means we need to change a ton of tests
to remove contact references.
2025-12-11 19:48:26 +00:00
gbrodman
a581259edb Remove trailing slash in schema-deploy script (#2903) 2025-12-11 18:32:09 +00:00
Pavlo Tkach
fcdac3e86e Update nomulus-frontend.yaml memory requests (#2900) 2025-12-10 22:36:28 +00:00
Nilay Shah
b652f81193 Refactor MosApiTLSKeySecretName configuration to the correct name (#2899) 2025-12-10 11:28:46 +00:00
Nilay Shah
d98d65eee5 Add mosapi client to intract with ICANN's monitoring system (#2892)
* Add mosapi client to intract with ICANN's monitoring system

This change introduces a comprehensive client to interact with ICANN's Monitoring System API (MoSAPI). This provides direct, automated access to critical registry health and compliance data, moving Nomulus towards a more proactive monitoring posture.

A core, stateless MosApiClient that manages communication and authentication with the MoSAPI service using TLS client certificates.

* Resolve review feedback & upgrade to OkHttp3 client

This commit addresses and resolves all outstanding review comments, primarily encompassing a shift to OkHttp3, security configuration cleanup, and general code improvements.

* **Review:** Addressed and resolved all pending review comments.
* **Feature:** Switched the underlying HTTP client implementation to [OkHttp3](https://square.github.io/okhttp/).
* **Configuration:** Consolidated TLS Certificates-related configuration into the dedicated configuration area.
* **Cleanup:** Removed unused components (`HttpUtils` and `HttpModule`) and performed general code cleanup.
* **Quality:** Improved exception handling logic for better robustness.

* Refactor and fix Mosapi exception handling

Addresses code review feedback and resulting test failures.

- Flattens package structure by moving MosApiException and its test.
- Corrects exception handling to ensure MosApiAuthorizationException
  propagates correctly, before the general exception handler.
- Adds a default case to the MosApiException factory for robustness.
- Uses lowercase for placeholder TLDs in default-config.yaml.

* Refactor and improve Mosapi client implementation

Simplifying URL validation with Guava
Preconditions and refining exception handling to use `Throwables`.

* Refactor precondition checks using project specific utility
2025-12-09 16:29:05 +00:00
gbrodman
28e72bd0d0 Add a BulkDomainTransferAction (#2893)
This will be necessary if we wish to do larger BTAPPA transfers (or
other types of transfers, I suppose). The nomulus command-line tool is
not fast enough to quickly transfer thousands of domains within a
reasonable timeframe.
2025-12-08 20:28:25 +00:00
gbrodman
0777be3d6c Allow superuser ext to override client/server transfer prohibited (#2890)
The superuser can remove/add those statuses anyway, so there's not
really any point. This also saves us trouble if we need to do a BTAPPA
transfer.
2025-12-05 20:22:15 +00:00
Weimin Yu
f9cd167ae4 Copy artifacts for schema tests after deployment (#2895)
After each deployment in sandbox or production, move the artifacts from
the corresponding release to a well-known location so that they can be
mapped to Kokoro in presubmit tests. The Kokoro-mapping does not need
public access to the GCS bucket.

The artifacts include the  postgresql schema jar, the nomulus release
jar, and the uber jar of the nomulus schema integration test classes.

Every jar name consists of a fixed prefix and the environment. Each jar
of a new deployment overrides the previous copy.
2025-12-04 20:55:19 +00:00
sharma1210
eed1886121 Implement rdap_query command (#2886)
* Implement rdap_query command

* modifying and correcting issues

* modifying and correcting issues

* modifying and correcting issues

* resolving comments

* resolving comments

* resolving comments

* resolving comments

* resolving comments

* modifying and correcting issues

* resolving comments

* resolving comments

* resolving comments

* modifying and correcting issues

* modifying and correcting issues

* modifying and correcting issues

* resolving comments

* modifying and correcting issues

* resolving comments

* Fixing Deduplication in test

* Fixing Deduplication in test

* resolving comments
2025-12-01 20:45:57 +00:00
gbrodman
7149fd3307 Remove more references to GAE (#2894)
These are old/pointless now that we've migrated to GKE. Note that this
doesn't update anything in the docs/ folder, as that's a much larger
project that should be done on its own.
2025-12-01 16:43:49 +00:00
Weimin Yu
0dc7ab99d7 Update CreateCdnsTld command for RST Tests (#2891)
Add a flag indicating that a Sandbox TLD should use the production
servers.

No additional TLD name pattern checks. Cloud DNS has an allowlist for
names that may use production servers.

Also updated default descriptive name generation: dropping the trailing
'.', and replacing remaining dots with '_'.
2025-11-25 19:41:44 +00:00
Ben McIlwain
76d4dfbb04 Add "augmented_latin.txt" IDN table in existing txt table format (#2884)
This contains the same codepoints from the
core/src/main/java/google/registry/idn/Latin-IDN.xml file, just in the old .txt
IDN format that Nomulus actually ingests.
2025-11-24 21:26:05 +00:00
gbrodman
8547ad7941 Remove the concept of a GAE service endpoint (#2869)
We don't need to support the mix of GAE and GKE any more so we can get
rid of the GaeService bits and unify everything under one constant
service. This also allows us to reduce the number of services down to
four (FE, BE, PUBAPI, console) which is nice.
2025-11-18 19:31:40 +00:00
gbrodman
b1266c95e8 Add and default to Argon2 hashing (#2877)
We've previously been using Scrypt since PR #2191 which, while being a
memory-hard slow function, isn't the optimal solution according to the
OWASP recommendations. While we could get away with increasing the
parallelization parameter to 3, it's better to just switch to the
most-recommended solution if we're switching things up anyway.

For the transition, we do something similar to PR #2191 where if the
previous-algorithm's hash is successful, we re-hash with Argo2id and
store that version. By doing this, we should not need any intervention
for registrars who log in at any point during the transition period.

Much of this PR, especially the parts where we re-hash the passwords in
Argon2 instead of Scrypt upon login, is based on the code that was
eventually removed in #2310.
2025-11-17 20:11:22 +00:00
Weimin Yu
bc9aab6790 Reformat Fee extension v1.0 schema (#2888)
Reformat the current schema file for RFC 8748 final version. This was
adapted from v0.12 is not fully consistent with the final schema

This helps highlight the differences we missed in PR 2855 when we check
in the official schema.
2025-11-17 15:58:56 +00:00
Ben McIlwain
6cb669a5a7 Remove Tld table field allowed_registrant_contact_ids (#2871)
This is a follow-up to PR #2867, requiring merging/deployment in a subsequent release.

BUG= http://b/448619572
2025-11-14 21:32:36 +00:00
Weimin Yu
0f92e98028 Disable Fee version 1.0 (#2887)
The v1.0 support added in PR 2855 is buggy. Disable it for now.
2025-11-14 20:32:45 +00:00
Ben McIlwain
5f0526c07a Make RDE generation resilient to missing contact rows (#2883)
This will prevent RDE from failing once we delete all contacts, just as a
fail-safe.

BUG= http://b/439636188
2025-11-13 20:09:43 +00:00
gbrodman
759aaddb5f Replace Front/Back-end servlets with single TestServlet (#2874)
The servlets, at this point now that we're off GAE, are only used for
the test server (and, indirectly, in one BSA test). Instead of having
them all remain separate, we can unify them in one test servlet that
lives in the test/ folder.

This removes one avenue of potential confusion w/r/t how request routing
actually works and where we would want to add new routing.
2025-11-12 21:01:14 +00:00
Ben McIlwain
816180f3b3 Remove more vestiges of GAE build (#2881) 2025-11-12 20:31:54 +00:00
Ben McIlwain
bf66b374c6 Address ICANN feedback on proposed Latin IDN table (#2880) 2025-11-10 20:33:29 +00:00
Weimin Yu
666cee1d9f Fix broken release script (#2878)
go/r3pr/2870 deleted release/cloudbuild-deploy.yaml.

Remove references to the file in the release script.
2025-11-07 20:59:05 +00:00
Weimin Yu
d4a70c29a8 Upload compatibility test jars to release folder (#2873)
We need to stop using maven repo on GCS to store artifacts for the
schema compatibility tests. After public access is removed from GCS
buckets, Kokoro won't be able to access it: normal access will be
denied, and the repo is too large to map (copy) to Kokoro VM as a
resource.

This PR uploads the relevant jars to each release's folder. See
go/dr-gcs-public-access-prevention for details.
2025-11-06 15:33:02 +00:00
gbrodman
7b8d07954b Remove more old-console-related files (#2866) 2025-11-05 19:43:59 +00:00
gbrodman
34bea69a48 Remove no-longer-used servlets/components (#2868)
With GKE, we don't need the individual servlets because the services
aren't partitioned out the same way they were in GAE.

We keep FrontendServlet and BackendServlet around for now as they serve
as the backbone for the local RegistryTestServer (for testing things
like the console).

did some cursory tests on alpha and things seem to be unaffected -- I
was able to curl RDAP (pubapi) and create domains
2025-11-05 19:03:45 +00:00
Ben McIlwain
363800bd86 Remove App Engine build/environment scripts (#2870)
It's been over half a year now since we last used any of these and we definitely
no longer have any intentions of ever using App Engine again.

BUG= http://b/457471639
2025-11-05 19:02:24 +00:00
gbrodman
dee132d04b Rename visibleInWhois fields to visibleInRdap (#2863)
Still part of b/454947209, removing references to WHOIS where we can. We
keep the registrar type and the column names (at least for now) because
changing those is much more complicated.
2025-11-04 17:37:44 +00:00
Ben McIlwain
847ef12a4f Remove Tld.allowedRegistrantContactIds field (#2867)
We no longer need this now that no contacts can be applied to any domains at all.

A follow-up PR in subsequent weeks will delete the column from the DB schema.

BUG= http://b/448619572
2025-10-31 15:52:10 +00:00
gbrodman
d9349be18e Modify the way we load resources via foreign keys (#2852)
Previously, we would have separate database calls for mapping from
foreign key to repo ID and then from repo ID to object. This PR modifies
those calls to load the resource directly (the old system was an
artifact of the Datastore key-value storage system).

In this PR, we merge the load-resource-by-foreign-key calls into a
single database load, as well as adding a separate cache object for
(foreign key) -> (resource). Now we cache, and have separate cleaner
code paths, for fk -> resource, fk -> repo ID, and repo ID -> resource.

Also removes the unused RdeFragmenter class
2025-10-29 19:21:27 +00:00
gbrodman
0c74883428 Remove WHOIS references from activity reporting (#2865)
This also changes a screenshot test filename since there wasn't a great
other place to put that change
2025-10-29 16:42:16 +00:00
Weimin Yu
b357fc79f7 Support Fee Extension standard in rfc 8748 (#2855)
* Support Fee Extension standard in rfc 8748

Adding support to the final version of RFC 8748.

Compared with draft-0.12, the only meaningful change is in the namespace.
The rest is either schema-tightening that reflects actual usage, or
optional server-side features that we do not support.

We reuse draft-0.12 tests, only changing namespace uris in the input and
output files for the new version.

* Addressing reviews
2025-10-28 20:54:02 +00:00
gbrodman
754e7fbddc Remove old console soy/js and related files (#2861)
We haven't been serving this for a while, let's finally get rid of them.

We keep some Soy rules around in the presubmits file because we use some
Soy files as XML templates for EPP actions.
2025-10-28 20:34:34 +00:00
Ben McIlwain
ad07b32638 Refactor EppResourceUtils.loadByForeignKey(...) -> ForeignKeyUtils.loadResource(...) (#2864)
This doesn't make any underlying implementation details, and is mainly useful to
reduce the number of diffs in PR #2852 (which does change implementation
details) thus making that easier to review.
2025-10-28 19:47:37 +00:00
Ben McIlwain
8f69b48e87 Add a @GetterDelegate annotation for better handling of ImmutableObject fields (#2860)
This allows us to specify a getter delegation to bypass Hibernate's limitations
on field types for the purposes of, e.g., using a sorted set in toString()
output rather than the base Hibernate unsorted HashSet type.

BUG=http://b/448631639
2025-10-28 17:10:27 +00:00
Ben McIlwain
c33f0dc07f Remove all foreign key constraints pointing to contact tables (#2857)
This affects FKs pointing to both Contact and ContactHistory. This is in
preparation to us deleting all rows in those two tables, and then subsequently
removing all application logic having to do with contacts entirely.
2025-10-27 20:34:50 +00:00
Juan Celhay
969353d4e2 Fix documentation for renew probers cb file (#2862) 2025-10-27 20:24:07 +00:00
gbrodman
6cd351ec7c Remove WHOIS classes and configuration (#2859)
This is steps one and two of b/454947209

We already haven't been serving WHOIS for a while, so there's no point
in keeping the old code around. This can simplify some code paths in the
future (like, certain foreign-key-loads that are only used in WHOIS
queries).
2025-10-27 18:57:25 +00:00
Pavlo Tkach
19e03dbd2e Update proxy and nomulus cluster resources (#2858) 2025-10-24 20:19:41 +00:00
Pavlo Tkach
fc1eb162f2 Remove Primary Contact from users editing screen (#2856) 2025-10-24 20:12:18 +00:00
gbrodman
ed25854fbc Add unique index for not-deleted domain names (#2853)
This is a backstop against multiple domain creations for the same domain
name getting through
2025-10-24 15:38:23 +00:00
Juan Celhay
0aa6bc6aaa Change regex format in release cb file (#2854) 2025-10-23 19:49:29 +00:00
Juan Celhay
ff4c326ebe Delete step to push to release repo, trigger next release steps based on tag format (#2833)
* Change release cb file

* Add brackets around tag variable

* Redo tag matching

* Have tag matcher like the one in cb dev
2025-10-21 18:52:42 +00:00
Pavlo Tkach
51b579871a Anonymize support users in console history, add minor UI updates (#2851) 2025-10-17 18:57:40 +00:00
gbrodman
b144aafb22 Use transaction time for deletion time cache ticker (#2848)
Basically, what happened is that the cache's expireAfterWrite was being
called some number of milliseconds (say, 50-100) after the transaction
was started. That method used the transaction time instead of the
current time, so as a result the entries were sticking around 50-100ms
longer in the cache than they should have been.

This fix contains two parts, each of which I believe would be sufficient
on their own to fix the issue:
1. Use the currentTime passed in in Expiry::expireAfterCreate
2. Use the transaction time in the cache's Ticker. This keeps everything
   on the same schedule.
2025-10-16 20:01:17 +00:00
Weimin Yu
ddd955e156 Fix dependency of Gradle task for schema test (#2849)
Problem not showing up because all use cases run this test after
`build`.
2025-10-16 15:33:28 +00:00
gbrodman
6863f678f1 Allow Gradle to use more heap space (#2847)
During the release process, we are seeing the message "Gradle build daemon disappeared unexpectedly (it may have been killed or may have crashed)" which seemingly can be caused by OOMs
2025-10-13 18:25:08 +00:00
gbrodman
6bd90e967b Add more hash indexes used during common flows (#2845)
I analyzed SQL statements run during the following flows and EXPLAIN
ANALYZEd each of them to figure out if there are any additional hash
indexes we could add that could be particularly helpful. Note: it's not
worth adding a hash index on the host_repo_id field in DomainHost
because so many rows (domains) use the same host.

- domain create
- domain delete
- domain info
- domain renew
- domain update
- host create
- host delete
- host update

I skipped the ones that use the read-only replica, as well as contact
flows (we're getting rid of them), and domain transfer/restore-related
flows as those are extremely infrequent.
2025-10-13 18:07:47 +00:00
gbrodman
5faf3d283c Differentiate between inserts and updates in flows (#2846)
Updates (AKA merges) run an extra SELECT statement to figure out if the
resource exists so that it can merge the entity into the existing object
in Hibernate's schema. When we're inserting new rows (such as new poll
messages or resource creates), we know that we don't need to do that
merge. Doing this should save us some SELECT statements (this has borne
out to be the truth in alpha)
2025-10-13 15:43:18 +00:00
gbrodman
149fb66ac5 Add cache for deletion times of existing domains (#2840)
This should help in instances of popular domains dropping, since we
won't need to do an additional two database loads every time (assuming
the deletion time is in the future).
2025-10-09 17:22:24 +00:00
gbrodman
8c96940a27 Only load from ClaimsList once when filling the cache (#2843) 2025-10-09 16:57:21 +00:00
Ben McIlwain
9c5510f05d Add a rate limiter to remove all domain contacts action (#2838)
The maximum QPS defaults to 10, but can also be specified at runtime through
use of a query-string parameter.

BUG = http://b/439636188
2025-10-02 22:15:19 +00:00
gbrodman
84884de77b Verify existence of TLDs and registrars for tokens (#2837)
Just in case someone makes a typo when running the commands
2025-10-02 20:10:58 +00:00
Ben McIlwain
d6c35df9bc Ignore single domain failures in remove contacts from all domains action (#2836)
When running the action in sandbox on 1.5M domains, it failed a few times
updating individual domains (requiring a manual restart of the entire action).
It's better to just log the individual failures for manual inspection and then
otherwise continue running the action to process the vast majority of other
updates that won't fail.

BUG = http://b/439636188
2025-10-02 18:58:23 +00:00
Juan Celhay
7caa0ec9d6 Add environment configuration files to .gitignore (#2830)
* Add environment configuration files to .gitignore

* Delete config files from repo

* Refactor release cb file to delete config file lines from gitignore

* Reorder env files

* Add README for config files
2025-10-02 18:36:43 +00:00
Weimin Yu
ee3866ec4a Allow top level tld creation in Sandbox (#2835)
Add a flag to the CreateCdnsTld command to bypass the dns name format
check in Sandbox (limiting names to `*.test.`). With this flag, we
can create TLDs for RST testing in Sandbox.

Note that if the new flag is wrongly set for a disallowed name, the
request to the Cloud DNS API will fail. The format check in the command
just provides a user-friendly error message.
2025-10-01 14:20:33 +00:00
gbrodman
97d0b7680f Add hash indexes for common use cases (#2834)
I went through all the SQL statements generated by some sample
DomainCreateFlow and DomainDeleteFlow cases to find situations where we
were either SELECTing from, or UPDATEing, tables with a direct "field =
value" format. These are the situations that I found where we can add
hash indexes. This does two things:

1. Makes these queries slight faster, since these are usually queries on
   columns that are either unique or very close to unique, and O(1) is
   faster than O(log(n))
2. Spreads around the optimistic predicate locks on the previously-used
   btree indexes. Many of our serialization errors came from the fact
   that we were autogenerating incrementing ID values for various
   tables, meaning that SELECTs, INSERTs, and UPDATEs would all try to
   take predicate locks out on the same page of the btree index. Using a
   hash index means that the page locks will be spread out to various
   index pages, rather than conflicting with each other.

Running load tests on alpha I see significant improvements in speed and
error rates. Speed is hard to quantify due to the nature of the way the
load tests distribute tasks among the queues but it could be more than
50% improvement, and serialization errors in the logs drop by more than
90%.
2025-09-29 22:16:24 +00:00
Pavlo Tkach
5700a008d6 Add console history frontend (#2832) 2025-09-26 21:25:03 +00:00
Ben McIlwain
dc9f5b99bc Add a batch action to remove all contacts from domains (#2827)
This implements the first part of Minimum Data Set phase 3, wherein we delete
all contact data. This action is necessary to leave a permanent record on the
domain (in the form of a domain history entry) documenting when the contacts
were removed by the administrative user.

Then, after this has finished removing all contact assocations, we can simply
empty out or drop the Contact/ContactHistory tables and associated join tables.
2025-09-25 20:47:17 +00:00
Ben McIlwain
d3c6de7a38 Modify the base Latin LGR with our intended changes to improve security (#2829) 2025-09-24 21:04:37 +00:00
Ben McIlwain
3c3303c16a Add ICANN's reference Latin LGR in RFC 7940 XML format (#2828)
In the next commit I will make changes to this file so it supports just the
basic Latin characters that we want, but it's good to check the base version in
so that we can see diffs.

This was downloaded from https://www.icann.org/sites/default/files/packages/lgr/lgr-second-level-latin-script-25oct24-en.xml
2025-09-19 16:42:46 +00:00
Nilay Shah
2a86a1bbe9 Skip user loading for proxy service account (#2825)
* Skip user loading for proxy service account

Reduces database load by skipping the User entity lookup for the proxy
service account during OIDC authentication.

The high volume of EPP "hello" and "login" commands from the proxy
service account results in a constant database load. These lookups
are unnecessary as the proxy service account is not expected to have a
corresponding User object.

This change optimizes the authentication flow by checking for the proxy
service account email *before* attempting to load a User from the
database. This bypasses the database transaction entirely for these
high-volume requests.

This approach is more efficient than caching, as it eliminates the
database lookup for the proxy service account altogether, rather than
just caching the result.

* comment added and service account llokup time improved

* comment updated for more clarity
2025-09-16 18:48:39 +00:00
gbrodman
ea148ac13e Show success message on password reset (#2826) 2025-09-16 18:39:19 +00:00
Nilay Shah
06299ccb86 Add cache for User entities in OIDC auth flow (#2822)
* Add cache for User entities in OIDC auth flow

* refactor: Address review feedback

- Refactor database call into a single, reusable method
- Increase the default cache size to 200
- Remove .recordStats() and using spy for testing
- Split unit tests into separate implementation test that use Mockito spies instead of checking internal cache stats
2025-09-12 07:43:32 +00:00
gbrodman
732c30b359 Remove registry-lock-related fields from RegistrarPoc (#2818)
We've moved these over to the User class, so we should remove these for
clarity. In addition, we should make it clear (in Java at least) that
the field in the RegistryLock object refers to the email address used
for the lock in question.
2025-09-11 15:29:06 +00:00
gbrodman
ee5a2d3916 Include internal registrars in the console (#2821)
This allows us to also check / modify the CharlestonRoad registrar in
the console, and also allows us to test actions (like password reset)
using that registrar in the prod environment.
2025-09-05 20:37:23 +00:00
gbrodman
2b5643df4c Sort registrars list in console (#2820)
This was bugging me slightly
2025-09-05 18:44:17 +00:00
Pavlo Tkach
6bbd7a2290 Update proxy resources, increase ssl handshake timeout (#2819) 2025-09-05 18:09:55 +00:00
Weimin Yu
77ab80f3dc Fix OOM in UploadBsaUnavailableDomains action (#2817)
* Fix OOM in UploadBsaUnavailableDomains action

The action was using string concatenation to generate the upload content.
This causes an OOM when string length exceeds 25MB on our current VM.

This PR witches to streaming upload.

Also added an HTTP upload test.

* Fix OOM in UploadBsaUnavailableDomains action

The action was using string concatenation to generate the upload content.
This causes an OOM when string length exceeds 25MB on our current VM.

This PR witches to streaming upload.

Also added an HTTP upload test.
2025-09-03 18:25:56 +00:00
Pavlo Tkach
5e1cd0120f Adjust proxy resource allocation and update nomulus compute class (#2814) 2025-08-28 18:49:16 +00:00
Weimin Yu
0167dad85f Fix OOM error in BsaValidation (#2813)
Error happened in the case that an unblockable name reported with
'Registered' as reason has been deregistered. We tried to check the
deletion time of the domain to decide if this is a transient error
that is no worth reporting. However, we forgot that we do not have
the domain key in this case.

As best-effort action, and with a case that rarely happens, we decide
not to make the optimization (staleness check) in thise case.
2025-08-27 15:47:13 +00:00
Weimin Yu
1eaf3d4aa8 Fix the schema-verifier in Cloud Build (#2812)
This is the same problem as the one that broke the Java test:

pg_dump upgrade to 17.6 added new meta command lines.
2025-08-25 17:17:49 +00:00
Pavlo Tkach
d9c46170dd Add time-limited logs to Epp Login flow to test increased latency (#2810) 2025-08-22 20:30:53 +00:00
gbrodman
e8a475f48b Remove Contact objects in RDAP output (#2811)
Note: this still includes "contacts" for registrars, which are actually
a different concept that we call RegistrarPoc. That's different from
"Contact" objects, e.g. registrant.
2025-08-22 20:25:20 +00:00
gbrodman
bdaab9daa5 Tag more junit versions to <6.0 (#2809)
We're still picking up some 6.0.0-M2 jars which are causing failures at
least for me when trying to run individual tests in IDEA.
2025-08-22 02:55:59 +00:00
gbrodman
7e07fabf7e Return RDAP 404 for domain w/nonexistent TLD (#2808)
The TLD is technically valid but it doesn't exist for us -- we should
return 404 instead of 400 in these situations according to the RDAP
conformance docs
2025-08-21 15:31:51 +00:00
gbrodman
16859bb36a Use https in RDAP URLs provided (#2807)
Load balancer / internal redirections can result in the final request
URL lacking "https" when finally getting to the servlet. As a result,
even if you use https in the request, the resulting URL can be plain
http.

We need to include the actual (HTTPS) URL in the output, so replace it.
2025-08-20 14:15:54 +00:00
Weimin Yu
7c92928f2c Update gradle dependency locks (#2806)
Also emoved Junit-4.
2025-08-19 16:17:47 +00:00
Pavlo Tkach
de8d205657 Make k8s adjustments (#2803)
This increases hikari fetch size to 40 from 20 in order to decrease the
amount of round trips
This also sets lower CPU as we seem to have overshot CPU consumtion
This also set min replicas to 8 for EPP and max to 16 as we've been
running on 8-10 for the last week
2025-08-19 14:24:11 +00:00
gbrodman
4738b979e4 Add FE for password-reset verification (#2795)
Tested locally and on alpha with dummy values (and throwing an
exception).

I was able to reuse a bit of code from the EPP password reset, but not
all of it.
2025-08-19 03:00:44 +00:00
Ben McIlwain
a61a667992 Add expiry_access_period_enabled boolean column to Tld table (#2804)
This is the first in a series of PRs to implement the expiry access period
(XAP).  The overall fee schedules will be set in YAML config files, so the only
DB change necessary should be this single new boolean column on the Tld entity,
which defaults to false so as to require XAP explicitly being turned on for a
given TLD.

BUG=http://b/437398822
2025-08-18 22:32:46 +00:00
Weimin Yu
1164070576 Update golden schema comparison in schema test (#2805)
Postgresql-17.6 introduces two new lines in pg_dump output as a
security feature: `\restricted {HASH}` and `\unrestricted {HASH}`.

We filter out lines starting with these two prefixes when comparing
schemas.

The db upgrade also adds two empty lines to the pg_dump output. We
know ignore all empty lines when comparing schemas.
2025-08-18 20:41:47 +00:00
gbrodman
d23640a54f Update LoadTestAction for GKE and no-contacts (#2800)
It's necessary to remove the GAE-related code (and use GKE launch
commands instead), and we might as well remove contact-related fields
and actions because of the upcoming move to the minimum data set.
2025-08-13 18:35:46 +00:00
Pavlo Tkach
cc347264f1 Revert "Remove nodeSelector from k8s deployments (#2798)" (#2799)
This reverts commit 5cef2dd8b5.
We faced CPU quota issue with standard machine type, so rolling back to c4
for now to monitor server performance and decide if we want to try to
downgrade again in the future.
2025-08-13 15:05:54 +00:00
Pavlo Tkach
e5d4cbb9fc Increase hikari maximum pool size (#2802) 2025-08-12 21:01:28 +00:00
gbrodman
8c1e0ff4de Chage run-time of DeleteExpiredDomainsAction (#2801)
We probably want this to run before the billing recurrence expansion
pipeline just in case there are any domains that should be deleted
before their billing recurrence gets expanded.
2025-08-12 20:48:04 +00:00
Pavlo Tkach
5cef2dd8b5 Remove nodeSelector from k8s deployments (#2798)
nodeSelector can limit scheduling capabilities of k8s, which leads to delays in assigning new workloads. Since we do not require and particular machine for execution it can be removed.
2025-08-10 16:16:48 +00:00
gbrodman
62b2585220 Fix load-testing URL in backend routing k8s file (#2797) 2025-08-08 20:20:40 +00:00
sharma1210
8692fe35db Provide specific reason for invalid SSL certificate (#2792)
* Fix: Robustly parse certs and provide specific errors

* Add test for expired certificate failure

* fixing indentation

* fixing indentation

* Update SecurityActionTest.java

* Update SecurityActionTest.java for correcting the testcase

* Fix: Provide indentation fix

* Fixing Deduplication in test
2025-08-08 18:41:14 +00:00
Pavlo Tkach
18614ba11e Update resource allocation for all Nomulus GKE deployments (#2796)
ALl deployments received update to averageUtilization cpu. This should allow us to stay ahead of the curve of traffic and create instances before we cpu reached the limit.
Frontend cpu allocation has caused "noise neighbors" problem with pods assigned to nodes where there's not enough bursting capacity, so I increased it.
Adjusted rest of the deployments according to their utilization.
2025-08-08 17:55:08 +00:00
Pavlo Tkach
427f6db820 Update resource allocation for proxy deployment (#2794)
Missing resource requests as well as metrics for when to evict resource
produced situation when under load k8s struggled to assign pods. This
adds default resource requirements based on 2 weeks metrics and
instructions when resource should be evicted.
2025-08-08 15:35:22 +00:00
Weimin Yu
5aa40b2208 Fix error handling in CopyDetailReportsAction (#2793)
* Fix error handling in CopyDetailReportsAction

The action tries to record errors per registrar in an ImmutableMap, without realizing that
there may be duplicate keys due to retries.

Switched to the `buildKeepingLast` method to build the map.

* Addressing comments and rebase
2025-08-06 16:43:29 +00:00
Pavlo Tkach
95c89bc856 Add registrar id header to proxy requests (#2791) 2025-08-05 17:57:04 +00:00
gbrodman
c21b66f0fb Add reset-EPP-password frontend component (#2786) 2025-08-01 19:53:20 +00:00
gbrodman
b070c46231 Allow superuser status to override EPP resource delete prohibited status (#2789) 2025-08-01 19:51:44 +00:00
Weimin Yu
a20eb8d1e9 Fix failures in retries when inserting new objects (#2788)
Given an entity with auto-filled id fields (annotated with
@GeneratedValue, with null as initial value), when inserting it
using Hibernate, the id fields will be filled with non-nulls even
if the transaction fails.

If the same entity instance is used again in a retry, Hibernate mistakes
it as a detached entity and raises an error.

The work around is to make a new copy of the entity in each transaction.
This PR applies this pattern to affected entity types.

We considered applying this pattern to JpaTransactionManagerImpl's insert
method so that individual call sites do not have to change. However, we
decided against it because:

- It is unnecessary for entity types that do not have auto-filled id

- The JpaTransactionManager cannot tell if copying is cheap or
  expensive. It is better exposing this to the user.

- The JpaTransactionManager needs to know how to clone entities. A new
  interface may need to be introduced just for a handful of use cases.
2025-08-01 18:53:50 +00:00
Ben McIlwain
338b8edb97 Make the contacts prohibited feature flag for min reg data set more lenient (#2787)
It will now only throw errors on domain updates if a new contact/registrant has
been specified where none was previously present. This means that domain updates
on unrelated fields (e.g. nameserver changes) will succeed even if there is
existing contact data that the update is not removing.

This is a follow-up to #2781.

BUG=http://b/434958659
2025-07-29 20:54:58 +00:00
gbrodman
9f191e9392 Add Registry Lock password reset on front end (#2785)
This is only enabled for admins, for now at least. It sends an email to
the registry lock email address to reset it.
2025-07-28 20:23:39 +00:00
gbrodman
39c2a79898 Remove superfluous DatabaseHelper db methods (#2784)
Some of these have been around since the Datastore days and are no
longer relevant (dealing with things like Datastore foreign keys). Let's
simplify things.
2025-07-25 17:00:24 +00:00
Pavlo Tkach
e2e9d4cfc7 Add console history api (#2782) 2025-07-18 18:46:21 +00:00
gbrodman
2948dcc1be Add password reset request and verify console actions (#2775)
This works fairly similarly to the registry lock request and
verification mechanism. The request action generates a UUI which is
emailed (in link form) to the user in question. The frontend will send a
request to the verify action with the UUID and hopefully the action
should be finalized.

EPP password requests can be sent by anyone with edit-registrar
permissions and must be approved by an admin POC email.

Registry lock password resets can only be sent by primary contacts, and
are verified/performed by the user in question.
2025-07-17 21:33:29 +00:00
Pavlo Tkach
c5644d5c8b Add stream to the console dum download (#2783) 2025-07-16 18:56:20 +00:00
Ben McIlwain
514d24ed67 Implement the contacts prohibited feature flag for minimum data set (#2781)
This prohibits all contact data on create and update EPP flows for both domain
and contact flows. It also refactors how default values on FeatureFlags work, as
it's safer to specify a single default on the flag itself rather than have to
specify it independently at a number of callsites (and potentially end up having
an inconsistent value). Domain updates on existing domains that still have
contact data will fail unless all contact data is removed, as a forcing function
to require registrars to rectify the situation prior to being able to do any
other kind of domain changes.

Contact-related flows that are still allowed after this point: Updating a domain
to remove all contacts from it, and deleting a contact object.
2025-07-14 15:29:14 +00:00
gbrodman
c6868b771b Update RDAP response profile + tech impl guide versions (#2778)
This corresponds to the Feb 2024 response profile section 1.2 and
implementation guide 1.3 respectively, now that we comply (or are, at
least closer to complying), with the Feb 2024 versions.

This should probably depend on https://github.com/google/nomulus/pull/2771
because that includes a small change included in the Feb 2024 version

This also updates the documentation to reference the proper areas of the
specifications.
2025-07-09 21:02:33 +00:00
gbrodman
f34aec8b56 Add an "about" link to registrars in RDAP (#2771)
From the response profile:
2.4.6. Registrar URL - The entity with the registrar role in the RDAP response
MUST contain a links member [RFC9083]. The links object MUST contain
the elements: value, identical to the the RDAP Base URL for the
Registrar as provided in the IANA “Registrar IDs” registry (i.e.,
https://www.iana.org/assignments/registrar-ids); rel:about, and href
containing the Registrar URL. Note: in cases where the Registry Operator
acts as sponsoring Registrar (e.g., IANA Registrar ID 9999), the href shall
contain a URL from the Registry.
2025-07-08 14:54:07 +00:00
Ben McIlwain
b27b077638 Increment proxy metrics by reciprocal of proxy metrics ratio (#2780)
This is necessary so that the total number of requests/responses adds up
correctly even though some fraction of them are only being recorded. It uses
stochastic rounding so that the totals add up correctly even when the reciprocal
of the ratio isn't an integer.

This is a follow-up to PR #2772.
2025-07-02 15:52:47 +00:00
Ben McIlwain
0e8cd75a58 Add the ability to configure a ratio of proxy metrics to be recorded (#2772)
This ratio defaults to 1.0 (i.e. all metrics will be recorded), but we will set
it much lower in sandbox and production, probably something closer to 0.01. This
will reduce recorded metrics volume and thus StackDriver cost, while still
retaining enough data for overall performance monitoring.

This is handled stochastically, so as to not require any coordination between
Java threads or GKE pods/clusters, as alternative approaches would (i.e. using a
counter and recording every Nth, or throttling to a max metrics qps).
2025-06-27 05:03:59 +00:00
gbrodman
2a1748ba9c Cache history values for RDAP domain requests (#2777)
In RDAP, domain queries are the most common by a factor of like 40,000
so we should optimize these as much as possible. We already have an EPP
resource / foreign key cache which does improve performance somewhat but
looking at some sample logs, it only cuts the RDAP request times by like
40% (looking at requests for the same domain a few seconds apart).

History entries don't change often, so we should cache them to make
subsequent queries faster as well. In addition, we're only caching two
fields per repo ID (modification time, registrar ID) so we can cache
more entries than we can for the EPP resource cache (which stores large
objects).
2025-06-25 19:33:36 +00:00
Weimin Yu
f4889191a4 Fix prober cert renewal scripts (#2776)
Scripts needed by cron jobs wrongly removed by PR 2661.

TESTED: in crash.
2025-06-25 13:51:06 +00:00
Weimin Yu
9eddecf70f Bypass config check for caching when safe (#2773)
Pubapi actions should always use cache, regardless of the config
settings on caching.

In EppResource.java, the original `loadCached(Iterable<VKey>)`
method is renamed to `loadByCacheIfEnabled`. The original
`loadCached(Vkey)` method is renamed to `loadByCache` and always
uses cache.

In EppResourceUtils.java, the original `loadByForeignKeyCached`
method is renamed to `loadByForeignKeyByCacheIfEnabled`. A new
`loadByForeignKeyByCache` method, which always uses cache.

In ForeighKeyUtils.java, the original `loadCached` method is
renamed to `loadByCacheIfEnabled`, and a new `loadCached` method
is added which always uses cache.

Also added a `getContactsFromReplica` method in Registrar,
for use by RDAP actions.
2025-06-20 21:25:02 +00:00
gbrodman
d4bcff0c31 Add password reset Java object (#2765)
A future PR will add the actions that save and use this object. That
future PR will also require loading RegistrarPoc objects given the
registrar ID, hence the change in that class.
2025-06-17 19:00:50 +00:00
Ben McIlwain
62065f88fb Remove spurious parenthesis in URS command output (#2767)
It was making the undo nomulus command look like this:

)nomulus ...
2025-06-16 20:23:48 +00:00
Pavlo Tkach
c9ac9437fd Add java code for RegitrarPoc id (#2770) 2025-06-14 17:37:11 +00:00
gbrodman
1f6a09182d Add some changes related to RDAP Feb 2024 profile (#2759)
This implements two type of changes:
1. changing the link type for things like the terms of service
2. adding the request URL to each and every link with the "value" field.
   This is a bit tricky to implement because the links are generated in
various places, but we can implement it by adding it to the results
after generation.

See b/418782147 for more information
2025-06-11 20:30:15 +00:00
Weimin Yu
a0eff00031 Add an aggregate module for DNS writers (#2769)
Add a new DnsWritersModule for use by the component classes.

To override the set of writers installed, we can easily overwrite this
file with a private version.
2025-06-09 14:46:54 +00:00
gbrodman
89698c6ed6 Update version of google-java-format (#2766)
This picks up a few changes including aligning the placement of quotes
in text blocks with the Google style guide.
2025-06-06 18:11:54 +00:00
gbrodman
a7696c3fac Add console action test base case (#2762)
We can probably improve on this in the future if we want, but there's a
lot of boilerplate that we don't need to repeat over and over
2025-06-04 15:36:22 +00:00
Weimin Yu
7ec599f849 Fix create_cdns_tld command (#2760)
The Cloud DNS rest api is now case-sensitive about enum names (must be
lower case, counterintuitively).
2025-06-03 15:17:43 +00:00
Pavlo Tkach
70291af9ad Add RegistrarPoc id column (#2761) 2025-06-02 15:43:03 +00:00
gbrodman
5fb95f38ed Don't always require contacts in CreateDomainCommand (#2755)
If contacts are optional, they should be optional in the command too.
2025-05-15 20:22:07 +00:00
gbrodman
dfe8e24761 Add registrar_id col to password reset requests (#2756)
This is just so that we can add an additional layer of security on
verification
2025-05-15 20:13:27 +00:00
Juan Celhay
bd30fcc81c Remove registrar id from invoice grouping key (#2749)
* Remove registrar id from invoice grouping key

* Fix formatting issues

* Update BillingEventTests
2025-05-13 20:29:25 +00:00
gbrodman
8cecc8d3a8 Use the primary DB for DomainInfoFlow (#2750)
This avoids potential replication lag issues when requesting info on
domains that were just created.
2025-05-13 18:00:30 +00:00
Pavlo Tkach
c5a39bccc5 Add Console POC reminder front-end (#2754) 2025-05-12 20:14:56 +00:00
gbrodman
a90a117341 Add SQL table for password resets (#2751)
We plan on using this for EPP password resets and registry lock password
resets for now.
2025-05-08 19:16:08 +00:00
Weimin Yu
b40ad54daf Hardcode beam pipelines to use GKE for tasks (#2753) 2025-05-08 17:29:30 +00:00
Pavlo Tkach
b4d239c329 Add console POC reminder backend support (#2747) 2025-04-30 14:15:43 +00:00
gbrodman
daa7ab3bfa Disable primary-contact editing in console (#2745)
This is necessary because we'll use primary-contact emails as a way of
resetting passwords.

In the UI, don't allow editing of email address for primary contacts,
and don't allow addition/removal of the primary contact field
post-creation.

In the backend, make sure that all emails previously added still exist.
2025-04-29 17:32:29 +00:00
gbrodman
56cd2ad282 Change AllocationToken behavior in non-catastrophic situations (#2730)
We're changing the way that allocation tokens work in suboptimal (i.e. incorrect) situations in the domain check, creation, and renewal process. Currently, if a token is not applicable, in any way, to any of the operations (including when a check has multiple operations requested) we return some variation of "Allocation token not valid" for all of those options. We wish to allow for a more lenient process, where if a token is "not applicable" instead of "invalid", we just pass through that part of the request as if the token were not there.

Types of errors that will remain catastrophic, where we'll basically return a token error immediately in all cases:
- nonexistent or null token
- token is assigned to a particular domain and the request isn't for that domain
- token is not valid for this registrar
- token is a single-use token that has already been redeemed
- token has a promotional schedule and it's no longer valid

Types of errors that will now be a silent pass-through, as if the user did not issue a token:
- token is not allowed for this TLD
- token has a discount, is not valid for premium names, and the domain name is premium
- token does not allow the provided EPP action

Currently, the last three types of errors cause that generic "token invalid" message but in the future, we'll pass the requests through as if the user did not pass in a token. This does allow for a default token to apply to these requests if available, meaning that it's possible that a single DomainCheckFlow with multiple check requests could use the provided token for some check(s), and a default token for others.

The flip side of this is that if the user passes in a catastrophically invalid token (the first five error messages above), we will return that result to any/all checks that they request, even if there are other issues with that request (e.g. the domain is reserved or already registered).

See b/315504612 for more details and background
2025-04-23 15:09:37 +00:00
gbrodman
0472dda860 Remove transaction duration logging (#2748)
We suspected this could be a cause of optimistic locking failures
(because long transactions would lead to optimistic locks not being
released) but this didn't end up being the case. Let's remove this to
reduce log spam.
2025-04-22 18:53:21 +00:00
gbrodman
083a9dc8c9 Remove old console history Java classes (#2726)
1. This doesn't remove the SQL tables yet (this is necessary to pass
   tests and also good practice just in case we need or want to look at
history for a little bit)
2. This also removes the Registrar, RegistrarPoc, and User base classes
   that were only necessary because we were saving copies of those
objects in the old history classes.
2025-04-18 22:05:29 +00:00
gbrodman
0153c6284a Add user objects for local test server (#2744)
Also don't try to do anything related to Google admin directory objects
when running the local test server, for obvious reasons
2025-04-18 15:48:06 +00:00
Pavlo Tkach
ca240adfb6 Add new last_poc_verification_date field to Registrar object (#2746) 2025-04-17 19:41:10 +00:00
Pavlo Tkach
b17125ae9a Disable k8s whois routing (#2740) 2025-04-17 15:20:32 +00:00
Pavlo Tkach
dfef733360 Incerase memory request for pubapi and frontend to 1Gi (#2743) 2025-04-11 16:17:43 +00:00
Pavlo Tkach
04a0659197 Disable console whois (#2741) 2025-04-11 15:32:34 +00:00
Pavlo Tkach
70010886b1 Increase hikari maximum pool size to 20 (#2742) 2025-04-10 20:51:51 +00:00
gbrodman
3cd50dc929 Only use GKE logs in ICANN reports (#2738)
We no longer need to union GKE+GAE logs since we've moved all production
traffic to GKE only.

For testing, I copied the affected *_test.sql files to Bigquery, removed
all the "-alpha" bits, and changed the dates to 20250301 and 20250331
and ran them to make sure they returned the expected data.
2025-04-09 17:12:02 +00:00
Pavlo Tkach
03872b508f Exclude prober endoint from sed command canary (#2739) 2025-04-07 21:13:13 +00:00
Pavlo Tkach
1096f201cd Add GKE readiness probe (#2735) 2025-04-04 21:33:43 +00:00
gbrodman
9dc3215624 Redirect an empty RDAP path to the /help response (#2722)
The behavior when someone hits the plain RDAP base URL isn't specified
by the spec. Currently we just return a plain 404 which isn't
particularly nice or helpful -- so it would probably be nicer to just
redirect to the /help response instead.

tested on alpha,
https://pubapi-dot-domain-registry-alpha.appspot.com/rdap redirects to https://pubapi-dot-domain-registry-alpha.appspot.com/rdap/help
2025-04-03 15:37:23 +00:00
Lai Jiang
af321fb65e Make frontend deployment auto scale (#2736)
Now that we have effective global sessions thanks to #2734, there is no
longer a need to keep the number of pods on the EPP service static.

We are also not vulnerable to random pod restarts. K8s never guarantees
perpetual pod lifetime anyway, and not having to be at its mercy is
certainly a relief.
2025-04-02 18:58:52 +00:00
Lai Jiang
c5132c04be Use pipe as extension URI separator (#2737)
It turns out period can be used in the URI, such as in
"urn:ietf:params:xml:ns:fee-0.12". I don't think pipe is used, at least
not according to EPP URI namespace naming convention.

Ideally we'd use serialization, but using the default serialization runs
the risk of it being platform/JDK dependent, so a new deployment might
not be able to deserialize existing cookies. A custom serializer that
guarantees stability would have been needed.
2025-04-02 13:21:13 +00:00
Lai Jiang
a64dc21f96 make the deploy task deploy to GKE (#2734)
Also always pulls the latest images from repos instead of relying on
local cases. This makes it so that a local docker build is always fresh.
2025-03-31 22:38:53 +00:00
Pavlo Tkach
0381533a35 Set grace period to 1s for immediate pods restart (#2733) 2025-03-31 19:15:13 +00:00
Lai Jiang
4999a72d96 Save session data directly in a cookie (#2732) 2025-03-31 16:21:50 +00:00
Pavlo Tkach
2d072c3844 Update jetty console static files cache policies (#2731) 2025-03-28 19:53:02 +00:00
Pavlo Tkach
c15dec4419 Downgrade node type for pubapi and console, enable bursting for frontend and backend (#2723) 2025-03-28 19:14:33 +00:00
gbrodman
8340125bf4 Remove user FKs from console history tables (#2729)
This, obviously, can mess up user deletion
2025-03-25 20:47:47 +00:00
Pavlo Tkach
98ba80d94e Remove console security settings timeout (#2728) 2025-03-25 19:36:52 +00:00
gbrodman
967d04efce Include TLD in reserved/registered lists too (#2725)
We already do this for premium terms, but it's nice to do it for the
other list types too

https://b.corp.google.com/issues/390053672
2025-03-24 15:52:12 +00:00
gbrodman
20fd944e83 Remove allocation token custom logic (#2727)
This was added back in early 2018 long ago to enable promotions, but
since then (and for many years) we've added the ability to run
promotions on the tokens themselves, rather than relying on custom Java
classes.

This will make the changes for b/315504612 much easier, as that will
split up token validation into "is this token valid in general?" and "is
this token valid for this domain/action?"
2025-03-21 20:48:54 +00:00
gbrodman
daa56e6d85 Bump the number of retries in transaction failures and add skew (#2699)
This can potentially help even more with serializable transaction
failures (optimistic locking exceptions, which are expected to occur
somewhat frequently).

With six attempts, we will sleep at most five times, for
100+200+400+800+1600 ms each, for a total of at most 3.1 seconds (much
less than the EPP maximum which I believe (?) to be 30 seconds.

In addition, we add a 20% skew in an attempt to spread out
possibly-conflicting transaction retries.
2025-03-21 19:47:55 +00:00
gbrodman
ed33c7424d Add and use new SimpleConsoleUpdateHistory table (#2712)
This changes the code to only save console histories of this type. We
keep the old Java code (and, necessarily, the corresponding SQL code)
for now because there's no harm in doing so and we want to avoid hastily
deleting too much.
2025-03-21 14:46:16 +00:00
Ben McIlwain
04b30f5c04 Fix handling of negative values in monthly transaction reporting (#2704)
The SQL statement was incorrectly flooring to zero one layer too deep, which was
negating all negative transaction report rows (which occur most frequently when
a domain in the autorenew grace period is deleted). I've changed it so that it
now only floors to zero at the report level, which still solves the issue
reported in http://b/290228682 but whose original fix caused the issue
http://b/344645788

This bug was introduced in https://github.com/google/nomulus/pull/2074

I tested this by running the new query against the DB for 2024 Q4 using the
registrar that was having issues and confirmed that the total renewal numbers
for .app now match with the sum total of what we invoiced for the last three
months of 2024.
2025-03-20 21:13:08 +00:00
Lai Jiang
11702bc940 Revert "Add a redirect for the console bare domain (#2718)" (#2724)
This reverts commit 2a01c12b14.
2025-03-19 22:48:31 +00:00
Lai Jiang
2d82646421 Uncap Dagger version (#2721)
The latest version of Dagger (2.55) now supports jakarta.inject.
2025-03-17 14:51:04 +00:00
Lai Jiang
50260dca5f Upgrade to Gradle 8.13 (#2720) 2025-03-15 00:30:32 +00:00
gbrodman
3cc10bfe0d Add a GCB script for monitoring ZFA accessibility (#2719)
This doesn't check for correctness (we have other scripts that do that)
but just that the service is available at all (the other scripts do not
do that).

This should, and will, be configured with a scheduled trigger in GCB (for us, in
the domain-registry-dev project) and configuration to send some sort of
pub/sub notification on failure (for us, this is already set up on
domain-registry-dev and it sends messages to the "Domain Registry
Notifications" chat channel.
2025-03-14 20:35:39 +00:00
Pavlo Tkach
5645b2e218 Embed Google Sans font (#2716) 2025-03-14 19:08:12 +00:00
Lai Jiang
2a01c12b14 Add a redirect for the console bare domain (#2718) 2025-03-14 18:16:25 +00:00
Lai Jiang
93d77e558f Update README (#2717) 2025-03-14 15:46:42 +00:00
Lai Jiang
92ebd0dedb Build different console versions for different environments (#2715)
TESTED=deployed to alpha
2025-03-11 23:39:28 +00:00
Lai Jiang
b49e37feee Add a GCB job to delete GAE canary versions (#2714)
We've seen this issue happen more often than not recently, where GAE
canary deployment is stuck for about 10 min and the failed. The reason
is not clear, but delete the canary version prior to a deployment always
fixes the issue.
2025-03-11 14:14:11 +00:00
Lai Jiang
bede56598c Fix console build for GKE (#2713)
We use the $environment property to set the console config. If it is not
given, 'alpha' is used, which has the same effect as 'production'.

TESTED=ran :jetty:copyConsole with
-Penvironment=(sandbox|production|alpha) and checked the resulting js
file.
2025-03-11 00:03:12 +00:00
Lai Jiang
467d9c7bf1 Fix cookie logging logic (#2711)
Make the logic more robust by using regex capture groups.
2025-03-10 23:10:03 +00:00
gbrodman
e5ebe96c74 Add SQL code for simplified console update history table (#2710)
We'll remove the old ones, but this one adds the new simplified version
2025-03-07 19:40:19 +00:00
gbrodman
2ff4d97b0a Refactor console bulk domain action types (#2708)
This makes the action types a bit simpler -- this is possible because
we've reduced the scope of domain actions that we want to natively
support
2025-03-07 18:12:32 +00:00
gbrodman
6b0beeb477 Add BSA label to rdap-domain 404 responses for BSA domains (#2706) 2025-03-07 13:58:18 +00:00
Lai Jiang
d2d43f4115 Fix a Cloud Scheduler deployment bug (#2707)
For GKE all tasks should be on backend, BSA was on its own service
because of egress IP constraint.

Also made it possible to specify a timeout for the Cloud Scheduler job,
with the default (3m) suitable for most tasks.
2025-03-06 16:25:52 +00:00
Lai Jiang
12fd206c35 Update README.md (#2705) 2025-03-05 16:55:04 +00:00
Lai Jiang
a3f510d0db Log session cookies in metadata (#2703)
There are two session cookies, JSESSIONID, which is set by Jetty, and
GCLB, which is set by the Gateway.

In one session, every request other than the first one (the <hello>)
should have the same GCLB value, and every request after a successful
<login> should have the same JSESSIONID.

With these two metadata, we should be able to trace all requests that
*should* belong to the same session and debug issues with session
mismatch (if any).
2025-03-04 20:18:15 +00:00
gbrodman
fa54c26ee2 Log transaction durations (#2682)
There can be delays in releasing predicate locks when we have
transactions that are long-lived -- even delays in releasing predicate
locks acquired by shorter-lived transactions. Logging the transaction
duration will allow us to get a sense as to transaction durations during
busy times.
2025-03-04 13:15:15 +00:00
gbrodman
8896fb94f4 Use nomulus-gke tagging mechanism in sql-int tests (#2702)
Had to temporarily create the files in
gs://domain-registry-dev-deployed-tags but the automated release process
will take care of that soon
2025-03-04 04:05:53 +00:00
Pavlo Tkach
6c7bf5e5dd Enable Users and Domains actions, add email notification (#2700) 2025-02-28 21:57:49 +00:00
Pavlo Tkach
ea1e8d5cc5 Add console gzip compression to js,css and html files (#2696) 2025-02-27 22:52:10 +00:00
Lai Jiang
7fb846c5b0 Add headers to record WHOIS client IPs (#2695)
The headers can be used by Cloud Armor to perform IP-based rate
limiting.
2025-02-27 22:15:13 +00:00
Lai Jiang
5180095cb6 Reduce log level to info when no email is found from the OIDC token (#2694)
This can happen on public endpoints (in pubapi) where the service is
behind IAP but all users (including not-logged-in ones) are allowed. IAP
will add an OIDC token with no email field in the request header.
2025-02-26 22:17:45 +00:00
Lai Jiang
9fe64bf9ec Make ignoreLinesStartingWith varargs (#2691)
It still is a list, because we String::startsWith does not benefit from
the target being in a set.
2025-02-26 17:12:24 +00:00
Lai Jiang
0f3b62d5ce Change the sleep time between proxy rollout (#2689) 2025-02-26 04:48:52 +00:00
Ben McIlwain
bd4701647b Refactor logic out of domain create flow tests (#2688)
This removes logic from an inner helper method so that it becomes more clear
from callsites within each test exactly which behavior is expected from those
test conditions.
2025-02-25 19:54:56 +00:00
Lai Jiang
fb816d7a2c Make it possible to ignore comment lines when comparing schemas (#2690)
We now pin to postgreSQL v17 when running tests, which means that minor
version might increase without our intervention. This causes (at least)
the comment in the golden schema to change, and failing the test as a
result.

This PR adds the ability to strip lines that we deem as comment from the
comparison, so we don't have to do trivial upgrades to the gold schema
whenever there's minor version upgrade.
2025-02-25 16:58:26 +00:00
gbrodman
8fbf363195 Remove unused dummy PGP file (#2687)
This was previously used as a dummy value for testing / compilation but
it's not used any more.
2025-02-24 21:45:26 +00:00
Lai Jiang
397f800614 Connect to GKE by default from the tool (#2686) 2025-02-24 19:01:05 +00:00
Lai Jiang
bcf42bd287 Use static IPs for EPP endpoints (#2685)
These IPs are now provisioned by Terraform. Also delete the
get-endpoints.py script as it is no longer necessary.
2025-02-24 16:38:47 +00:00
Pavlo Tkach
ed95d19b93 Provide prompt for user deletion UI (#2684) 2025-02-21 20:30:03 +00:00
Lai Jiang
97fc2c0b66 Add an annotation to the deployment (#2683)
This allows us to easily tell which tag was deployed.

Also set the gateway to use named address so they are stable, and so
that we can attach an IPv6 record to it. Auto-provisioned addresses are
IPv4 only.
2025-02-21 16:30:32 +00:00
Weimin Yu
00728c40ba Abort schema verifier when pg_dump fails (#2681)
Failed pg_dump may not leave a file, failing the subsequent diffing and
causing the verifier to return success.

The verifier should abort in this case.
2025-02-20 17:35:47 +00:00
Lai Jiang
3f2a42ab8d Expose EPP via saidcar proxy (#2680) 2025-02-19 18:57:25 +00:00
Lai Jiang
b73e342820 Update PostgreSQL version in builder image and tests (#2667) 2025-02-18 17:34:41 +00:00
Lai Jiang
df7fec7a3e Update RDAP TOS link (#2678) 2025-02-18 17:00:26 +00:00
Lai Jiang
6f7ae1eabc Redirect HTTP to HTTPS (#2679)
This opens up port 80 on the load balancer IP and upgrades all HTTP
request to HTTPS.

TESTED=tested on alpha.
2025-02-18 16:57:18 +00:00
Lai Jiang
eb978ebbd5 Let nomulus tool connect to sandbox GKE by default (#2674) 2025-02-16 18:10:03 +00:00
Pavlo Tkach
95831bc8b7 Add suspend / unsuspend to the console (#2675) 2025-02-14 20:41:19 +00:00
Lai Jiang
538260521b Update Nomulus deployment script (#2677)
We only deploy to the us-central1 cluster in order to minimize database
locality issue.
2025-02-14 17:31:18 +00:00
Pavlo Tkach
612708f0a8 Fix console user creation role param (#2676) 2025-02-14 13:51:06 +00:00
Lai Jiang
e78de98060 Read GKE logs in ICANN reports (#2673)
GKE logs are routed to a different dataset and the table is different.
The structs to look for are also different (jsonPayload vs textPayload
or protoPayload).

TESTED=Ran the resulting query in crash.
2025-02-12 20:41:44 +00:00
Lai Jiang
c918258fb1 Make a best effort attempt to support multiple CPU architectures (#2672)
I obtained access to an IBM s390x VM so I thought I'd see how multi-arch
Nomulus is.

Our main application is in Java so it is already multi-arch, but several
tests use docker images that are by default x64. Luckily postgres has an
s390x port, but selenium does not. So I had to disable Screenshot tests
when the arch is not amd64.
2025-02-07 22:19:42 +00:00
gbrodman
34103ec815 Convert gsutil to gcloud storage (#2670)
Use of gsutil is discouraged / deprecated, see https://cloud.google.com/storage/docs/gsutil
2025-02-07 21:01:19 +00:00
Lai Jiang
a63812160e Upgrade to Gradle 8.12.1 (#2671) 2025-02-07 15:23:02 +00:00
gbrodman
9aaf7ee36a Allow for no fee extension with free premium domains (#2660)
This isn't a situation we'll encounter often, but if the client has an
allocation token that's valid for premium domains that gives a 0 cost,
we shouldn't require them to include the fee extension when creating the
domain. We already don't require it for standard domains.
2025-02-06 20:40:24 +00:00
gbrodman
96a864dbd6 Add pg_stat_statements extension to allowed diffs (#2662)
This is similar to pgaudit in that it doesn't need to exist in the
golden file.
2025-02-06 20:39:59 +00:00
Lai Jiang
8a36fb5f1f Update Cloud Scheduler and Cloud Tasks deployment process (#2666) 2025-02-06 18:53:50 +00:00
Pavlo Tkach
6c138420b0 Fix console nested routes a11y (#2669) 2025-02-05 20:45:21 +00:00
Lai Jiang
08570511f5 Update GCB scripts (#2661) 2025-02-04 19:27:44 +00:00
Pavlo Tkach
e62d970d34 Update console endpoints documentation (#2665) 2025-02-04 17:43:30 +00:00
Lai Jiang
067927b735 Fix GCB failures (#2664)
We start seeing failures such as this one:

https://pantheon.corp.google.com/cloud-build/builds;region=global/843b9bd7-9c09-4221-ae4c-6e2dd2918f04?inv=1&invt=Aborfg&project=domain-registry-alpha

It looks like the inclusion of gcompute-module which itself is a git
repo caused the problem. I don't understand why it wasn't an issue before.
My guess is that GCB started using a newer version of git which is more
strict about this.

TESTED=Tested the GCB build pipeline on alpha.
2025-02-04 17:12:43 +00:00
Pavlo Tkach
4ec2919ce3 Update console dependencies (#2659) 2025-01-31 21:40:37 +00:00
gbrodman
19422075fa Remove nested transactions from domain (un)locking (#2658) 2025-01-31 16:47:44 +00:00
Pavlo Tkach
40b6984ffb Improve console screen reader interaction (#2656) 2025-01-31 16:46:25 +00:00
Lai Jiang
6952e0f653 Fix a typo (#2657) 2025-01-31 02:44:28 +00:00
Lai Jiang
dcb55d27bb Upload gateway related manifests to GCS (#2655) 2025-01-30 16:12:31 +00:00
Pavlo Tkach
765bd9834a Add more accessible names to the console (#2652) 2025-01-29 20:19:00 +00:00
Lai Jiang
221088e738 Upload k8s manifests to GCS (#2654) 2025-01-29 17:07:10 +00:00
gbrodman
6649e00df7 Allow for particular flows to log all SQL statements executed (#2653)
We use this now for the DomainDeleteFlow in an attempt to figure out
what statements it's running (cross-referencing that with PSQL's own
statement logging to find slow statements).
2025-01-29 16:00:19 +00:00
gbrodman
2ceb52a7c4 Handle SPECIFIED renewal price w/token in check flow (#2651)
This is kinda nonsensical because this use case is trying to apply a
single use token multiple times in the same domain:check request --
like, trying to use a single-use token for both create, renew, and
transfer while having a $0 create price and a premium renewal price.

This change doesn't affect any actual business / costs, since SPECIFIED
token renewal prices were already set on the BillingRecurrence
2025-01-28 18:31:29 +00:00
Lai Jiang
120bcc33be Update cloud build configs to build nomulus images (#2650)
Also do appropriate text replacements for each environment.
2025-01-28 16:03:26 +00:00
Pavlo Tkach
8987fd37c2 Improve console accessibility (#2649) 2025-01-26 00:47:53 +00:00
gbrodman
653e092ad4 Add TLD identifier to premium terms filename and header (#2644)
https://b.corp.google.com/issues/390053672

This makes it easier to identify what file you're looking at, at a
glance
2025-01-24 19:54:35 +00:00
gbrodman
5e97a8b412 Refactor console domain actions to exist in separate files (#2638)
This means that we're not storing everything in one file, otherwise it
quickly becomes unwieldy
2025-01-23 16:46:53 +00:00
Weimin Yu
229fcf3946 UrlConnectionException loses error info (#2648)
It does not get the error message for 400+ status codes.

It fails to get the status code if the response has neither data nor
error.
2025-01-23 16:27:03 +00:00
Lai Jiang
b775e4a178 Pull credentials from fleet for all clusters (#2647)
All clusters have switched to using private APIs.
2025-01-22 16:58:56 +00:00
Pavlo Tkach
e3c386a8a7 Add console bulk delete (#2641)
* Add bulk actions to console

* Add console bulk delete

* Add console bulk delete
2025-01-22 15:54:59 +00:00
Lai Jiang
799f0449ad Only pull credential from the fleet on crash (#2645)
Only crash has the policy controller installed for now.
2025-01-21 18:40:52 +00:00
Lai Jiang
bf025445d5 Record http request parameters in log metadata (#2642)
This allows us to search for logs for a given path using a filter like
this:

jsonPayload.httpRequest.requestUrl="/_dr/blah"

TESTED=tested on crash
2025-01-16 17:27:53 +00:00
Lai Jiang
9f22f2e8ae Pull nomulus cluster credentials from the fleet (#2643)
After private endpoint is enabled, we cannot pull the credentials
directly via `gcloud containers cluster get-credentials`.
2025-01-16 15:06:02 +00:00
gbrodman
45c8b81823 Map token renewal behavior directly onto BillingRecurrence (#2635)
Instead of using a separate RenewalPriceInfo object, just map the
behavior (if it exists) onto the BillingRecurrence with a special
carve-out, as always, for anchor tenants (note: this shouldn't matter
much since anchor tenants *should* use NONPREMIUM renewal tokens anyway,
but just in case, double-check).

This also fixes DomainPricingLogic to treat a multiyear create as a
one-year-create + n-minus-1-year-renewal for cases where either the
creation or the renewal (or both) are nonpremium.
2025-01-15 19:55:34 +00:00
Weimin Yu
4cfcc60655 Clean up keyring bindings (#2640)
Remove the config file's `keyring` section and the binding in java code.
2025-01-14 22:06:05 +00:00
Lai Jiang
e4ee63b8f3 Make Cloud Tasks Utils canary-aware (#2639) 2025-01-14 17:39:51 +00:00
Weimin Yu
f8407c74bc Make SecretManagerkeyring the only allowed keyring (#2636)
Remove the support for custom keyrings. There is no pressing use case,
and can be error-prone.
2025-01-13 19:32:24 +00:00
gbrodman
693467a165 Remove duplicate transaction in updateAllocTokens (#2637) 2025-01-13 19:12:06 +00:00
Lai Jiang
cea3da01a0 Expose Web WHOIS redirects (#2634)
We are required to respond to HTTP(S) requests on port 80/443 on the
same domain where we serve port 43 WHOIS requests. The proxy already
does this by redirecting to the web WHOIS lookup page on the marketing
website.

This PR makes it so that requests to port 80/443 can be routed to the
proxy for redirect.

TESTED=tested on crash and the redirect works.
2025-01-10 17:25:16 +00:00
Weimin Yu
c2030e5859 Fix keyring in BEAM pipeline (#2632)
SecretManager based keyring not included in keyring bindings, resulting
in runtime failure.

We should simply keyring bindings. There is no use case for multiple
implementations. See b/388835696.
2025-01-09 20:01:32 +00:00
Lai Jiang
1cbbc660d2 Explicity specify deployment order for queues and scheduler tasks (#2631)
If we deploy Nomulus, we should do that before queues and the scheduler
tasks are updated.
2025-01-08 21:11:24 +00:00
Lai Jiang
e0bbff827e Upgrade to Gradle 8.12 (#2630) 2025-01-08 18:43:10 +00:00
Weimin Yu
10925f2447 Enable nested transaction warning in production (#2628)
Knonw nested transact calls found in sandbox have been refactored away.
Enable logging in production to catch any missing cases. Logging is
throttled at 1 message per minute per VM.
2025-01-03 20:52:25 +00:00
Lai Jiang
7641b05f12 Expose EPP and WHOIS endpoints on reginal load balancers (#2627)
k8s does not have a way to expose a global load balancer with TCP
endpoints, and setting up node port-based routing is a chore, even with
Terraform (which is what we did with the standalone proxy).

We will use Cloud DNS's geolocation routing policy to ensure that
clients connect to the endpoint closest to them.
2024-12-26 15:25:02 +00:00
Weimin Yu
d130e74004 Use sql instance name in SecretManager (#2625) 2024-12-18 18:39:23 +00:00
Lai Jiang
c9c61e4f17 Write GKE metrics with the apprioate labels (#2626)
Also makes preperations to expose the sidecar proxy.
2024-12-18 16:15:54 +00:00
Lai Jiang
da8df1f4d9 Make GKE the default in alpha and qa (#2624) 2024-12-17 17:40:03 +00:00
Pavlo Tkach
f649d960c1 Add user email prefix to the console user create (#2623) 2024-12-13 19:47:21 +00:00
Weimin Yu
e5ebc5a2bb Save Cloud SQL connection names in Keyring (#2622)
This eliminates the need to make a new release after database disaster
recovery.
2024-12-13 16:18:15 +00:00
Lai Jiang
f9d2839590 Add necessary changes to provision QA with Terraform (#2618)
Also programmatically determine backend service IDs.
2024-12-12 18:39:18 +00:00
gbrodman
c6a6bc7e25 Drop FKs referencing DomainHistory (#2621)
- We never delete rows from DomainHistory (and even if we do in the
  future, they'll be old / the references won't matter)
- This is likely creating lock contention when lots of requests come
  through at once for domains with many DomainHistory entries
2024-12-10 18:46:48 +00:00
gbrodman
fce126d426 Update Flyway versions to 11.x+ (#2620)
There are some breaking method changes in the 10.x.y versions and we're encountering exceptions when trying to run the flywayMigrate task thanks to those.
2024-12-10 03:02:02 +00:00
gbrodman
8e41278717 Include GP statuses in RDAP results (#2606)
We do this for WHOIS results so we should do it for RDAP results as well
(especially since they're mostly already included in the response
profile).
2024-12-09 19:55:16 +00:00
Lai Jiang
cb3738d540 Upgrade to Gradle 8.11.1 (#2619) 2024-12-09 18:29:57 +00:00
Pavlo Tkach
71afc25110 Fix console new user screen layout (#2617) 2024-12-05 18:17:52 +00:00
Pavlo Tkach
fa377733be Allow adding existing users to registrar (#2616) 2024-11-27 22:40:32 +00:00
gbrodman
21950f7d82 Add a bulk-domain-action console endpoint (#2611)
For now it only includes two options (domain deletion and domain
suspension). In the future, as necessary, we can add other actions but
this seems like a relatively simple starting point (actions like bulk
updates are much more conceptually complex).
2024-11-22 20:47:47 +00:00
Ben McIlwain
e66aee0416 Downgrade the tx isolation level of poll message ack flow (#2615)
This might help alleviate DB transaction contention on the PollMessage table. A
lower transaction isolation level is safe because acking a poll message is
idempotent: there are only two things it does, either delete a poll message or
take a recurring one from the past and set it to be a year in the future from
the date in the past. Both of these operations will always yield the same final
result even if executed multiple times simultaneously for some reason.
2024-11-22 19:48:19 +00:00
Ben McIlwain
c7e1fc17d2 Downgrade the tx isolation level of poll message request flow (#2614)
It doesn't need a higher transaction isolation level as it's only loading a given poll
message once, and we want to avoid putting any kind of locks on the PollMessage table
as it seems to be having contention issues. Note that the poll message request flow
is by far the most frequent code that touches the PollMessage table, as there are many
many requests every minute from dozens of registrars, but much fewer poll messages
than that to actually ACK.
2024-11-21 22:49:57 +00:00
gbrodman
0c0b0df36e Skip poll messages on deletions for configured registrars (#2613)
See b/379331882 for more details
2024-11-21 22:16:26 +00:00
Weimin Yu
304f0002b4 Refactor FlowRunner transaction invocation (#2612)
Stop calling `transact` if already in a transaction.
2024-11-21 15:58:26 +00:00
gbrodman
15cf3e1bc0 Add RegistrarUpdateHistory objects for console changes (#2585) 2024-11-19 21:03:48 +00:00
Pavlo Tkach
eeed166310 Add console user role update and minor fixes to delete (#2610) 2024-11-15 18:36:10 +00:00
gbrodman
e54075fea3 Allow for removal of registry lock passwords in User objects (#2609)
This essentially enables the "forgot password" flow
2024-11-14 21:01:17 +00:00
Ben McIlwain
78cc1b2937 Fix 'Domian' typo for Domain (#2608) 2024-11-12 18:41:56 +00:00
Pavlo Tkach
35f95bbbe4 Add delete user to the console (#2603)
* Add delete user to the console

* Add delete user to the console

* Add delete user to the console
2024-11-08 18:20:01 +00:00
gbrodman
ae61cd443d Sometimes include deletion times in domain-list exports (#2602)
We only include the deletion time if the domain is in the 5-day
PENDING_DELETE period after the 30 day REDEMPTION period. For all other
domains, we just have an empty string as that field.

This is behind a feature flag so that we can control when it is enabled
2024-11-06 17:59:30 +00:00
gbrodman
cc20f7d76d Add a simple toString for TimedTransitionProperty (#2604)
this means that we can actually see the transitions when running
GetAllocationTokenCommand, for instance
2024-11-05 18:26:36 +00:00
Ben McIlwain
5603b91526 Make nomulus update_recurrence command only fail on pending transfers (#2605)
It was failing when any kind of transfer data was present, even completed
transfer data. Note that completed transfer data persists on a domain
indefinitely until/unless a new transfer is requested.

BUG= http://b/377328244
2024-11-04 21:16:11 +00:00
Pavlo Tkach
332f491ac7 Fix cut off status list on domains page (#2601) 2024-10-28 18:20:04 +00:00
Pavlo Tkach
4bd7c18fe9 Add console settings update progress status (#2596) 2024-10-25 22:23:22 +00:00
Pavlo Tkach
fdb0664841 Add admin.directory.user.security scope (#2597) 2024-10-25 21:24:15 +00:00
Lai Jiang
a9ba770bfa Add canary service to GKE (#2594) 2024-10-22 17:12:00 +00:00
Lai Jiang
4d96e5a6b1 Remove cap on soy (#2592)
We still need to cap the protobuf version that soy depends on, but the
rest of nomulus can use the latest version of protobuf.
2024-10-18 17:21:26 +00:00
Lai Jiang
1171c5cfcb Delete legacy console (#2579) 2024-10-17 20:48:10 +00:00
Pavlo Tkach
91e241374d Add required fields to API users().insert (#2593) 2024-10-17 19:45:12 +00:00
Weimin Yu
634202c0e9 A batch query utility to replace TransactionManager's loadAllOf methods (#2589)
* Replace  with batch query

* Addressing CR
2024-10-14 20:11:29 +00:00
Lai Jiang
020ed33003 Fix releases (#2591)
It seems like `/usr/bin/python` is no longer symlinked to the `python3`
binary in the `gcr.io/cloud-builders/git` image.

I've sent out a separate fix to upstream to change the shebang.

https://gerrit-review.git.corp.google.com/c/gcompute-tools/+/439501

But in the meantime, we need this temporary fix for the release to
build.
2024-10-14 15:42:15 +00:00
dependabot[bot]
0f61066b1d Bump the npm_and_yarn group in /console-webapp with 3 updates (#2588)
Bumps the npm_and_yarn group in /console-webapp with 3 updates: [cookie](https://github.com/jshttp/cookie), [socket.io](https://github.com/socketio/socket.io) and [express](https://github.com/expressjs/express).


Updates `cookie` from 0.4.2 to 0.7.2
- [Release notes](https://github.com/jshttp/cookie/releases)
- [Commits](https://github.com/jshttp/cookie/compare/v0.4.2...v0.7.2)

Updates `socket.io` from 4.7.5 to 4.8.0
- [Release notes](https://github.com/socketio/socket.io/releases)
- [Changelog](https://github.com/socketio/socket.io/blob/main/CHANGELOG.md)
- [Commits](https://github.com/socketio/socket.io/compare/socket.io@4.7.5...socket.io@4.8.0)

Updates `express` from 4.21.0 to 4.21.1
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/4.21.1/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.21.0...4.21.1)

---
updated-dependencies:
- dependency-name: cookie
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: socket.io
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: express
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Lai Jiang <jianglai@google.com>
2024-10-11 01:38:56 +00:00
Pavlo Tkach
03711481cd Fix console screenshot tests flakiness (#2590) 2024-10-10 21:17:42 +00:00
gbrodman
c32fb2fc71 Add screenshot tests for the new registrar console (#2577)
This required updating to a newer version of Selenium, building the
console dist/ folder, and serving that folder.
2024-10-09 16:44:34 +00:00
Pavlo Tkach
6e77c89cd6 Add console users screen (#2576) 2024-10-08 16:00:47 +00:00
Lai Jiang
5e41e84b8d Upgrade avro version (#2587)
Per b/371714822.
2024-10-07 15:43:53 +00:00
Lai Jiang
bfd569ee44 Add Aman to CONTRIBUTORS (#2586)
Give credit where credit it due @sanger2000.
2024-10-04 22:46:25 +00:00
Lai Jiang
b13a33347f Add Juan to CONTRIBUTORS (#2584) 2024-10-03 23:59:10 +00:00
Lai Jiang
d17a6edf12 Try to fix CodeQL java actions (#2583)
These flags are suggested by GitHub support to disable reusing caches
during Gradle build. They think that could fix the intermittent error
message:

```
Encountered a fatal error while running "/opt/hostedtoolcache/CodeQL/2.19.0/x64/codeql/codeql database finalize --finalize-dataset --threads=4 --ram=14576 --verbosity=progress++ /home/runner/work/_temp/codeql_databases/java". Exit code was 32 and last log line was: CodeQL detected code written in Java/Kotlin but could not process any of it. For more information, review our troubleshooting guide at https://gh.io/troubleshooting-code-scanning/no-source-code-seen-during-build . See the logs for more details.
```
2024-10-03 19:54:26 +00:00
Lai Jiang
7255ebff29 Fix a redirect bug (#2582) 2024-10-03 16:51:54 +00:00
Pavlo Tkach
cacc90097a Set POST method to SyncRegistrarSheetAction invocation (#2580) 2024-10-02 15:32:38 +00:00
Lai Jiang
0ef8984767 Remove schedule CodeQL run (#2581)
This fails for me every day for some reason (starting about a month
ago). The same commit went through the workflow fine when the action was
triggered by a push.

I think there's no reason for us to have a cron run as the changes to the
master branch can only come from commit pushes.
2024-10-02 15:29:55 +00:00
Juan Celhay
7a4abd93dc Add discount price param to GenerateAllocationTokens command (#2578)
* Add discount price param to GenerateAlloCationTokens command

* add discount price param to UpdateAllocationTokens command
2024-10-01 22:20:21 +00:00
Lai Jiang
142c910e3b Disable legacy registrar console (#2575) 2024-10-01 19:42:35 +00:00
gbrodman
c68d54a5ed Don't show snackbar on rlock-load failure if 403 (#2574)
ACCOUNT_MANAGER users don't have permission to see locks so it'll throw
403s. That's OK, we don't need/want to display that error to the client.
2024-09-30 20:42:33 +00:00
Pavlo Tkach
d17188b820 Add console users action (#2573) 2024-09-30 15:39:38 +00:00
Lai Jiang
cbe59b6950 Upgrade to use Gradle setup-gradle v4 (#2569)
I have seen a lot of CodeQL errors recently for no apparent reason.
Hopefully this will fix them.
2024-09-26 20:45:07 +00:00
Ben McIlwain
2b3c6525ff Add some basic info logging to RefreshDnsForAllDomainsAction (#2572)
BUG=http://b/369842541
2024-09-26 19:03:08 +00:00
Lai Jiang
72dd8658cf Upgrade to Gradle 8.10.2 (#2571) 2024-09-26 17:53:45 +00:00
Lai Jiang
c0490f7777 Update google-java-format (#2570)
Also converted regex strings in the Python script to raw strings as
future Python versions will start to reject them.

See: https://docs.python.org/3/whatsnew/3.12.html#other-language-changes
2024-09-26 14:49:38 +00:00
Lai Jiang
a22a38527b Make GPG related tests work with the latest version of GPG (#2568)
Newer versions of GPG (v.2.4.5 in my case) has uses different wording
then what's available in our build image (and Ubuntu I suspect). For
example it says "rsa2048" instead of "2048-bit RSA".

Make the tests work in both cases. Admittedly we cannot check for the
string RSA/rsa easily, but I don't think it matters much for tests.
2024-09-26 14:10:07 +00:00
Lai Jiang
08203033a2 Make the db object sync job more reliable (#2567)
It looks like /usr/bin/python *may* no longer exists in the latest cloud
builder git image. I ran the latest image and logged into it to verify
that /usr/bin/python3 does exist on 9/25, and again on 9/26 where it
re-appeared.

I think it is generally a good idea to not rely on it being there going
forward.
2024-09-26 02:10:00 +00:00
dependabot[bot]
d0482a8f2c Bump rollup in /console-webapp in the npm_and_yarn group (#2566)
Bumps the npm_and_yarn group in /console-webapp with 1 update: [rollup](https://github.com/rollup/rollup).


Updates `rollup` from 4.16.4 to 4.22.4
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.16.4...v4.22.4)

---
updated-dependencies:
- dependency-name: rollup
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-25 18:47:21 +00:00
Juan Celhay
e6a2db8075 Add discount price to AllocationToken (#2559)
* Include discount price in domai n pricing

* Partial progress in logic

* Tests and logic passing

* Change pricing for multi year create

* Tests for discount pricing logic

* Token currency check

* Add some comments

* Java formatting

* Discount price to Optional

* Change discount price to be optional nullable

* Re-add deleted tests
2024-09-23 20:18:33 +00:00
Lai Jiang
7929322e95 Connect to the correct endpoints based on runtime (#2540)
* Connect to the correct endpoints based on runtime

* Address code review comments

* Add checks for HTTP methods
2024-09-20 18:39:54 +00:00
Lai Jiang
5c35811eb9 Upgrade protobuf (#2565)
Patched from https://github.com/google/nomulus/pull/2564.

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-20 17:56:53 +00:00
Weimin Yu
4ba0f4a2cd Change nested transact calls to retransact (#2563) 2024-09-20 15:16:52 +00:00
Lai Jiang
e167b4b753 Make the cloud scheduler deployer GKE-aware (#2562)
Depending on if a "--gke" parameter (must be the last one) is passed,
the deployer constructs the corresponding URIs for GAE or GKE
accordingly.

TESTED=Used the deployer to deploy tasks to alpha and verified that they
run on GKE.
2024-09-19 16:28:08 +00:00
Joaquin Gimenez
c47f821754 Fix typo in docs (#2520) 2024-09-18 18:57:27 +00:00
Weimin Yu
febdbc0468 Drop the transact call in IdService (#2561)
* Drop the `transact` call in Id services

All usages already routed through `tm().allocateId()`, which is
guaranteed to be in a transaction.

* Addressing reviews
2024-09-18 18:18:36 +00:00
dependabot[bot]
a988732d65 Bump the npm_and_yarn group in /console-webapp with 5 updates (#2560)
Bumps the npm_and_yarn group in /console-webapp with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [body-parser](https://github.com/expressjs/body-parser) | `1.20.2` | `1.20.3` |
| [express](https://github.com/expressjs/express) | `4.19.2` | `4.21.0` |
| [path-to-regexp](https://github.com/pillarjs/path-to-regexp) | `0.1.7` | `0.1.10` |
| [send](https://github.com/pillarjs/send) | `0.18.0` | `0.19.0` |
| [serve-static](https://github.com/expressjs/serve-static) | `1.15.0` | `1.16.2` |


Updates `body-parser` from 1.20.2 to 1.20.3
- [Release notes](https://github.com/expressjs/body-parser/releases)
- [Changelog](https://github.com/expressjs/body-parser/blob/master/HISTORY.md)
- [Commits](https://github.com/expressjs/body-parser/compare/1.20.2...1.20.3)

Updates `express` from 4.19.2 to 4.21.0
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/4.21.0/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.19.2...4.21.0)

Updates `express` from 4.19.2 to 4.21.0
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/4.21.0/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.19.2...4.21.0)

Updates `path-to-regexp` from 0.1.7 to 0.1.10
- [Release notes](https://github.com/pillarjs/path-to-regexp/releases)
- [Changelog](https://github.com/pillarjs/path-to-regexp/blob/master/History.md)
- [Commits](https://github.com/pillarjs/path-to-regexp/compare/v0.1.7...v0.1.10)

Updates `send` from 0.18.0 to 0.19.0
- [Release notes](https://github.com/pillarjs/send/releases)
- [Changelog](https://github.com/pillarjs/send/blob/master/HISTORY.md)
- [Commits](https://github.com/pillarjs/send/compare/0.18.0...0.19.0)

Updates `serve-static` from 1.15.0 to 1.16.2
- [Release notes](https://github.com/expressjs/serve-static/releases)
- [Changelog](https://github.com/expressjs/serve-static/blob/v1.16.2/HISTORY.md)
- [Commits](https://github.com/expressjs/serve-static/compare/v1.15.0...v1.16.2)

---
updated-dependencies:
- dependency-name: body-parser
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: express
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: express
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: path-to-regexp
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: send
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: serve-static
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-17 22:40:24 +00:00
Juan Celhay
7ee541e1b1 Add activeOrDeletedSince parameter to RefreshDnsForAllDomainsAction (#2556) 2024-09-17 16:02:25 +00:00
Lai Jiang
b07769bdee Switch to new jaxb runtime (#2558) 2024-09-16 22:39:59 +00:00
Weimin Yu
9db016638e Use repeatable-read in some batch actions (#2557) 2024-09-16 18:56:33 +00:00
Weimin Yu
c3d164d462 CheckApi should use replica (#2554) 2024-09-16 16:10:28 +00:00
Lai Jiang
352618b3b7 Use Jcommander 2.0 (#2552) 2024-09-13 16:06:50 +00:00
Lai Jiang
0389b0d2d9 Upgrade to Gradle 8.10.1 (#2533) 2024-09-11 21:36:12 +00:00
gbrodman
8906a82e3b Update dependencies esp. antlr+jcommander (#2550) 2024-09-11 15:49:54 +00:00
Weimin Yu
f6e42896c3 Build uber jars as multi-release (#2549)
Several jars in our dependencies are now multi-release, including
dnsjava and snakeyaml, and a few more. Such jars include
jvm-version-specific classes that will only be loaded by the vm that can
handle them. All it takes is a new manifest attribute.

This change allows us to upgrade to dnsjava3.6+: the base (java 8) version of
this jar breaks java21. The correct manifest allows java21 to find the
classes it needs.
2024-09-10 18:12:25 +00:00
Juan Celhay
4d3dec54cf First PR for adding discount price into AllocationToken. (#2547) 2024-09-06 20:46:04 +00:00
Weimin Yu
f082ffffe3 Suppress misleading error-level logs by hibernate (#2546)
Update logging configs after Hibernate 6 migration.
2024-09-06 18:10:04 +00:00
Ben McIlwain
5f23f2a15a Reduce cardinality of reserved list processing time metric (#2542)
This single metric currently accounts for 22.2% of our total metrics bill,
almost double the size of our EPP requests metric, while also simultaneously
being much less useful. This change reduces the cardinality by removing two
parameters we don't care that much about, which should significantly reduce the
size and thus the cost. If after this change the metric is still too large, I'll
also then remove the matchCount parameter from this metric. We could possibly
even consider deleting the metric in its entirety, as we hardly ever use it.

This PR also removes unused code for premium list metrics that have never
actually been written out (and that we won't bother with at this point).
2024-09-05 19:41:20 +00:00
Weimin Yu
7ed7cf3340 Fix BSA batch query for all unblockables (#2544)
Typo in sql script resulting in some unblockables not loaded.

See b/361770793
2024-09-05 19:33:26 +00:00
gbrodman
ab60ac44fd Migrate DNS query table (#2543)
Co-authored-by: Lai Jiang <jianglai@google.com>
2024-09-04 17:42:12 +00:00
Weimin Yu
d9ad39cdad Upgrade to Hibernate6 and Jaxb4 (#2526)
* Rebase

* Addressing commetns

* Addressing comments

* Auto convert Duration and Money
2024-09-03 18:02:53 +00:00
Juan Celhay
bac4e22bff Add retries to DriveConnection.listFiles() on GoogleJsonResponseExceptions (#2541)
* Add retries to DriveConnection.listFiles() on GoogleJsonResponseExceptions

* Remove unused import

* Remove unread variable

* Add comment inputs

* fix formatting

* Remove period from error message.
2024-09-03 14:27:00 +00:00
Pavlo Tkach
ab5f6cc229 Add environment support to the console build (#2539) 2024-08-30 18:31:28 +00:00
gbrodman
1765f4f0b4 Allow skip of emailing/uploading for activity reports (#2538)
This will help us if/when we need to run the report generation multiple
times, or for past dates and we don't want to send extra emails or
upload any extra reports to ICANN.
2024-08-26 20:25:31 +00:00
gbrodman
e88c6e1550 Update activity/txn reporting to use new GAE log format (#2535)
Instead of having to parse the protoPayload.line from the request logs,
we just want to inspect the textPayload from the app logs (stored in a
separate table). This applies to the EPP metrics from the activity
reporting and the attempted-adds column for the transaction reporting.
2024-08-26 19:41:40 +00:00
Pavlo Tkach
1739c6d74f Update node.js to v22 (#2537) 2024-08-26 18:15:39 +00:00
Pavlo Tkach
66513a114e Add OT&E UI to the new console (#2536) 2024-08-23 20:53:45 +00:00
Pavlo Tkach
0e808a4c01 Add OT&E create and status to the new console (#2534) 2024-08-22 20:03:56 +00:00
Lai Jiang
4e013603be Make GKE networking work more properly (#2531) 2024-08-22 13:10:56 +00:00
gbrodman
730585cd14 Fix front-end unit tests (#2529)
This doesn't really add any tests, and we'll require many more additions
if we actually want to have full unit testing, but this at least makes
the tests pass when running `npm test`.
2024-08-21 16:39:29 +00:00
gbrodman
fd7820759d Use token's renewalPrice if renewalBehavior is SPECIFIED (#2502)
Previous PRs and token changes (see b/332928676) have made it so that
SPECIFIED renewalPriceBehavior tokens must have a renewal price. As
such, we can now use that renewalPrice when creating domains with
SPECIFIED tokens.
2024-08-15 19:06:32 +00:00
sarahcaseybot
69359bb1e6 Add QPS and incomplete connections metrics to load test client (#2487)
* Add QPS and incomplete connections metrics to load test client

* Add a failed request count

* Add todos

* Reuse contact

* Add bugs to todos

* small fix

* Clarify QPS
2024-08-14 18:14:17 +00:00
gbrodman
35b602a76e Remove User ID field from SQL (#2523)
This will fail tests until the corresponding PR in Java is deployed.
2024-08-14 17:51:15 +00:00
dependabot[bot]
82002d1f75 Bump axios in /console-webapp in the npm_and_yarn group (#2532)
Bumps the npm_and_yarn group in /console-webapp with 1 update: [axios](https://github.com/axios/axios).


Updates `axios` from 1.7.2 to 1.7.4
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.7.2...v1.7.4)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-14 15:44:14 +00:00
Lai Jiang
2fd9b062df Make Nomulus work on GKE with external load balancer (#2527)
This will create a multi-cluster external load balancer exposing HTTP
traffic to nomulus running in clusters in the fleet.
2024-08-14 14:32:39 +00:00
Ben McIlwain
ec3804e87e Make domain update flow handle null auth data (#2530)
It's valid for the auth data to be null (although it only happens 10 times
across our entire registry), so the domain update flow should not fail out with
a NullPointerException when the existing state of the data is null and the
update isn't adding that data either.

BUG=http://b/359264787
2024-08-13 18:19:44 +00:00
2218 changed files with 54824 additions and 84897 deletions

View File

@@ -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
View File

@@ -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

View File

@@ -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>

View File

@@ -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)

View File

@@ -1,121 +0,0 @@
// Copyright 2019 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
apply plugin: 'war'
def environment = rootProject.environment
def gcpProject = rootProject.gcpProject
// Set this directory before applying the appengine plugin so that the
// plugin will recognize this as an app-engine standard app (and also
// obtains the appengine-web.xml from the correct location)
project.convention.plugins['war'].webAppDirName =
"../../core/src/main/java/google/registry/env/${environment}/${project.name}"
apply plugin: 'com.google.cloud.tools.appengine'
def coreResourcesDir = "${rootDir}/core/build/resources/main"
def coreLibsDir = "${rootDir}/core/build/libs"
// Get the web.xml file for the service.
war {
webInf {
from "../../core/src/main/java/google/registry/env/common/${project.name}/WEB-INF"
}
}
war {
from("${coreResourcesDir}/google/registry/ui/html") {
include "*.html"
}
from("${coreLibsDir}") {
include "core.jar"
into("WEB-INF/lib")
}
}
if (project.path == ":services:default") {
war {
from("${coreResourcesDir}/google/registry/ui") {
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.mustRunAfter ':console-webapp:deploy'
appengineDeployAll.finalizedBy ':deployCloudSchedulerAndQueue'
rootProject.deploy.dependsOn appengineDeployAll
rootProject.stage.dependsOn appengineStage
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

View File

@@ -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.
//
@@ -555,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 {
@@ -563,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}"
}
}

View File

@@ -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']
}

View File

@@ -3,7 +3,7 @@
# This file is expected to be part of source control.
aopalliance:aopalliance:1.0=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
com.github.ben-manes.caffeine:caffeine:3.0.5=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
com.github.ben-manes.caffeine:caffeine:3.1.8=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
com.github.ben-manes.caffeine:caffeine:3.2.2=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
com.github.kevinstern:software-and-algorithms:1.0=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
com.google.auto.service:auto-service-annotations:1.0.1=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
com.google.auto.value:auto-value-annotations:1.11.0=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
@@ -12,13 +12,13 @@ com.google.auto:auto-common:1.2.1=annotationProcessor,errorprone,testAnnotationP
com.google.code.findbugs:jsr305:3.0.2=annotationProcessor,checkstyle,compileClasspath,deploy_jar,errorprone,runtimeClasspath,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath,testing,testingAnnotationProcessor,testingCompileClasspath
com.google.errorprone:error_prone_annotation:2.23.0=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
com.google.errorprone:error_prone_annotations:2.23.0=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
com.google.errorprone:error_prone_annotations:2.28.0=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
com.google.errorprone:error_prone_annotations:2.40.0=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
com.google.errorprone:error_prone_annotations:2.7.1=checkstyle
com.google.errorprone:error_prone_check_api:2.23.0=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
com.google.errorprone:error_prone_core:2.23.0=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
com.google.errorprone:error_prone_type_annotations:2.23.0=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
com.google.errorprone:javac:9+181-r4173-1=errorproneJavac
com.google.flogger:flogger:0.8=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
com.google.flogger:flogger:0.9=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
com.google.guava:failureaccess:1.0.1=annotationProcessor,checkstyle,errorprone,testAnnotationProcessor,testingAnnotationProcessor
com.google.guava:failureaccess:1.0.2=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
com.google.guava:guava-parent:32.1.1-jre=annotationProcessor,errorprone,testAnnotationProcessor,testingAnnotationProcessor
@@ -30,41 +30,41 @@ com.google.inject:guice:5.1.0=annotationProcessor,errorprone,testAnnotationProce
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.3=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.jspecify:jspecify:0.3.0=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath,testing,testingCompileClasspath
org.junit.jupiter:junit-jupiter-api:5.11.0-M2=testCompileClasspath,testRuntimeClasspath
org.junit.jupiter:junit-jupiter-engine:5.11.0-M2=testCompileClasspath,testRuntimeClasspath
org.junit.platform:junit-platform-commons:1.11.0-M2=testCompileClasspath,testRuntimeClasspath
org.junit.platform:junit-platform-engine:1.11.0-M2=testCompileClasspath,testRuntimeClasspath
org.junit:junit-bom:5.11.0-M2=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=jacocoAnt
org.ow2.asm:asm:9.7=compileClasspath,deploy_jar,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

View File

@@ -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);

View File

@@ -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. */

View File

@@ -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. */

View File

@@ -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')

View File

@@ -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"
},
@@ -265,6 +280,9 @@
{
"moduleLicense": "Unicode-3.0"
},
{
"moduleLicense": "The W3C Software License"
},
{
"moduleLicense": "Public Domain",
"moduleName": "aopalliance:aopalliance"
@@ -277,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
@@ -290,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.
@@ -297,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,
@@ -354,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".
@@ -393,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"
}
]
}

View File

@@ -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 = [

View File

@@ -25,7 +25,11 @@ import textwrap
import re
# We should never analyze any generated files
UNIVERSALLY_SKIPPED_PATTERNS = {"/build/", "cloudbuild-caches", "/out/", ".git/", ".gradle/", "/dist/", "karma.conf.js", "polyfills.ts", "test.ts", "/docs/console-endpoints/"}
UNIVERSALLY_SKIPPED_PATTERNS = {"/build/", "cloudbuild-caches", "/out/", ".git/",
".gradle/", "/dist/", "/console-alpha/", "/console-crash/", "/console-qa",
"/console-sandbox", "/console-production", "karma.conf.js", "polyfills.ts",
"test.ts", "/docs/console-endpoints/", "/bin/generated-sources/",
"/bin/generated-test-sources/", "src/main/generated", "src/test/generated"}
# We can't rely on CI to have the Enum package installed so we do this instead.
FORBIDDEN = 1
REQUIRED = 2
@@ -87,25 +91,22 @@ PRESUBMITS = {
PresubmitCheck(
r".*Copyright 20\d{2} The Nomulus Authors\. All Rights Reserved\.",
("java", "js", "soy", "sql", "py", "sh", "gradle", "ts"), {
".git", "/build/", "/bin/generated-sources/", "/bin/generated-test-sources/",
"node_modules/", "LoggerConfig.java", "registrar_bin.",
".git", "/build/", "node_modules/", "LoggerConfig.java", "registrar_bin.",
"registrar_dbg.", "google-java-format-diff.py",
"nomulus.golden.sql", "soyutils_usegoog.js", "javascript/checks.js",
"/src/main/generated", "/src/test/generated"
"nomulus.golden.sql", "soyutils_usegoog.js", "javascript/checks.js"
}, REQUIRED):
"File did not include the license header.",
# Files must end in a newline
PresubmitCheck(r".*\n$", ("java", "js", "soy", "sql", "py", "sh", "gradle", "ts"),
{"node_modules/"}, REQUIRED):
PresubmitCheck(r".*\n$", ("java", "js", "soy", "sql", "py", "sh", "gradle", "ts", "xml"),
{"node_modules/", ".idea"}, REQUIRED):
"Source files must end in a newline.",
# System.(out|err).println should only appear in tools/ or load-testing/
PresubmitCheck(
r".*\bSystem\.(out|err)\.print", "java", {
"StackdriverDashboardBuilder.java", "/tools/", "/example/",
"/load-testing/", "RegistryTestServerMain.java",
"TestServerExtension.java", "FlowDocumentationTool.java"
"/tools/", "/example/", "/load-testing/",
"RegistryTestServerMain.java", "TestServerExtension.java"
}):
"System.(out|err).println is only allowed in tools/ packages. Please "
"use a logger instead.",
@@ -118,7 +119,7 @@ PRESUBMITS = {
):
"In SOY please use the ({@param name: string} /** User name. */) style"
" parameter passing instead of the ( * @param name User name.) style "
"parameter pasing.",
"parameter passing.",
PresubmitCheck(
r'.*\{[^}]+\w+:\s+"',
"soy",
@@ -137,41 +138,6 @@ PRESUBMITS = {
{},
):
"All soy templates must use strict autoescaping",
# various JS linting checks
PresubmitCheck(
r".*goog\.base\(",
"js",
{"/node_modules/"},
):
"Use of goog.base is not allowed.",
PresubmitCheck(
r".*goog\.dom\.classes",
"js",
{"/node_modules/"},
):
"Instead of goog.dom.classes, use goog.dom.classlist which is smaller "
"and faster.",
PresubmitCheck(
r".*goog\.getMsg",
"js",
{"/node_modules/"},
):
"Put messages in Soy, instead of using goog.getMsg().",
PresubmitCheck(
r".*(innerHTML|outerHTML)\s*(=|[+]=)([^=]|$)",
"js",
{"/node_modules/", "registrar_bin."},
):
"Do not assign directly to the dom. Use goog.dom.setTextContent to set"
" to plain text, goog.dom.removeChildren to clear, or "
"soy.renderElement to render anything else",
PresubmitCheck(
r".*console\.(log|info|warn|error)",
"js",
{"/node_modules/", "google/registry/ui/js/util.js", "registrar_bin."},
):
"JavaScript files should not include console logging.",
PresubmitCheck(
r".*\nimport (static )?.*\.shaded\..*",
"java",
@@ -208,6 +174,12 @@ PRESUBMITS = {
{"/node_modules/"},
):
"Do not use javax.servlet.* Use jakarta.servlet.* instead.",
PresubmitCheck(
r".*javax\.inject\..*",
"java",
{"/node_modules/"},
):
"Do not use javax.inject.* Use jakarta.inject.* instead.",
}
# Note that this regex only works for one kind of Flyway file. If we want to
@@ -295,26 +267,6 @@ def verify_flyway_index():
return not success
def verify_javascript_deps():
"""Verifies that we haven't introduced any new javascript dependencies."""
with open('package.json') as f:
package = json.load(f)
deps = list(package['dependencies'].keys())
if deps != EXPECTED_JS_PACKAGES:
print('Unexpected javascript dependencies. Was expecting '
'%s, got %s.' % (EXPECTED_JS_PACKAGES, deps))
print(textwrap.dedent("""
* If the new dependencies are intentional, please verify that the
* license is one of the allowed licenses (see
* config/dependency-license/allowed_licenses.json) and add an entry
* for the package (with the license in a comment) to the
* EXPECTED_JS_PACKAGES variable in config/presubmits.py.
"""))
return True
return False
def get_files():
for root, dirnames, filenames in os.walk("."):
for filename in filenames:
@@ -339,8 +291,5 @@ if __name__ == "__main__":
# when we put it here it fails fast before all of the tests are run.
failed |= verify_flyway_index()
# Make sure we haven't introduced any javascript dependencies.
failed |= verify_javascript_deps()
if failed:
sys.exit(1)

View File

@@ -44,3 +44,4 @@ Thumbs.db
# Build artifact
/staged/dist
/staged/console-*

View File

@@ -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

View File

@@ -63,11 +63,50 @@
],
"outputHashing": "all"
},
"development": {
"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,
"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"
@@ -78,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"
}

View File

@@ -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,14 +101,9 @@ task checkFormatting(type: Exec) {
args 'run', 'prettify:check'
}
task deploy(type: Exec) {
workingDir "${consoleDir}/staged"
executable 'gcloud'
args 'app', 'deploy', "${projectParam}", '--quiet'
}
tasks.buildConsoleWebappProd.dependsOn(tasks.npmInstallDeps)
tasks.buildConsoleWebapp.dependsOn(tasks.npmInstallDeps)
tasks.runConsoleWebappUnitTests.dependsOn(tasks.npmInstallDeps)
tasks.applyFormatting.dependsOn(tasks.npmInstallDeps)
tasks.checkFormatting.dependsOn(tasks.npmInstallDeps)
tasks.build.dependsOn(tasks.checkFormatting)
tasks.deploy.dependsOn(tasks.buildConsoleWebappProd)
tasks.build.dependsOn(tasks.runConsoleWebappUnitTests)

View File

@@ -1,7 +1,7 @@
{
"/console-api":
{
"target": "http://localhost:8080",
"target": "http://[::1]:8080",
"secure": false,
"logLevel": "debug",
"changeOrigin": true

View File

@@ -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

View File

@@ -0,0 +1 @@
configuration=production

View File

@@ -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: [

File diff suppressed because it is too large Load Diff

View File

@@ -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": "^18.0.2",
"@angular/cdk": "^18.0.2",
"@angular/common": "^18.0.2",
"@angular/compiler": "^18.0.2",
"@angular/core": "^18.0.2",
"@angular/forms": "^18.0.2",
"@angular/material": "^18.0.2",
"@angular/platform-browser": "^18.0.2",
"@angular/platform-browser-dynamic": "^18.0.2",
"@angular/router": "^18.0.2",
"@angular/animations": "^19.1.4",
"@angular/cdk": "^19.1.2",
"@angular/common": "^19.1.4",
"@angular/compiler": "^19.1.4",
"@angular/core": "^19.1.4",
"@angular/forms": "^19.1.4",
"@angular/material": "^19.1.2",
"@angular/platform-browser": "^19.1.4",
"@angular/platform-browser-dynamic": "^19.1.4",
"@angular/router": "^19.1.4",
"rxjs": "~7.5.0",
"tslib": "^2.3.0",
"zone.js": "~0.14.2"
"zone.js": "~0.15.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "^18.0.3",
"@angular-eslint/builder": "18.0.1",
"@angular-eslint/eslint-plugin": "18.0.1",
"@angular-eslint/eslint-plugin-template": "18.0.1",
"@angular-eslint/schematics": "18.0.1",
"@angular-eslint/template-parser": "18.0.1",
"@angular/cli": "~18.0.3",
"@angular/compiler-cli": "^18.0.2",
"@angular-devkit/build-angular": "^19.1.5",
"@angular-eslint/builder": "19.0.2",
"@angular-eslint/eslint-plugin": "19.0.2",
"@angular-eslint/eslint-plugin-template": "19.0.2",
"@angular-eslint/schematics": "19.0.2",
"@angular-eslint/template-parser": "19.0.2",
"@angular/cli": "~19.1.5",
"@angular/compiler-cli": "^19.1.4",
"@types/jasmine": "~4.0.0",
"@types/node": "^18.11.18",
"@types/node": "^18.19.74",
"@typescript-eslint/eslint-plugin": "^7.2.0",
"@typescript-eslint/parser": "^7.2.0",
"concurrently": "^7.6.0",
@@ -52,6 +52,6 @@
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.0.0",
"prettier": "2.8.7",
"typescript": "~5.4.5"
"typescript": "^5.7.3"
}
}
}

View File

@@ -17,27 +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 { RegistryLockVerifyComponent } from './lock/registryLockVerify.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',
@@ -45,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,
@@ -69,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,
},
],
},
// {
@@ -112,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,

View File

@@ -7,18 +7,19 @@
></mat-progress-bar>
</div>
<mat-sidenav-container class="console-app__container">
<mat-sidenav-content class="console-app__content-wrapper">
<div class="console-app__content" role="main">
<router-outlet></router-outlet>
</div>
</mat-sidenav-content>
<mat-sidenav
[mode]="breakpointObserver.isMobileView() ? 'over' : 'side'"
[opened]="!breakpointObserver.isMobileView()"
[disableClose]="!breakpointObserver.isMobileView()"
#sidenav
class="console-app__sidebar"
>
<app-navigation />
</mat-sidenav>
<mat-sidenav-content class="console-app__content-wrapper">
<div class="console-app__content">
<router-outlet></router-outlet>
</div>
</mat-sidenav-content>
</mat-sidenav-container>
</div>

View File

@@ -14,29 +14,124 @@
import { provideHttpClient } from '@angular/common/http';
import { provideHttpClientTesting } from '@angular/common/http/testing';
import { TestBed } from '@angular/core/testing';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RouterTestingModule } from '@angular/router/testing';
import { ComponentFixture, fakeAsync, TestBed } from '@angular/core/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { AppComponent } from './app.component';
import { MaterialModule } from './material.module';
import { BackendService } from './shared/services/backend.service';
import { routes } from './app-routing.module';
import { AppModule } from './app.module';
import { PocReminderComponent } from './shared/components/pocReminder/pocReminder.component';
import { RouterModule } from '@angular/router';
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
import { UserData, UserDataService } from './shared/services/userData.service';
import { Registrar, RegistrarService } from './registrar/registrar.service';
import { MatSidenavModule } from '@angular/material/sidenav';
import { signal, WritableSignal } from '@angular/core';
describe('AppComponent', () => {
let component: AppComponent;
let fixture: ComponentFixture<AppComponent>;
let mockRegistrarService: {
registrar: WritableSignal<Partial<Registrar> | null | undefined>;
registrarId: WritableSignal<string>;
registrars: WritableSignal<Array<Partial<Registrar>>>;
};
let mockUserDataService: { userData: WritableSignal<Partial<UserData>> };
let mockSnackBar: jasmine.SpyObj<MatSnackBar>;
const dummyPocReminderComponent = class {}; // Dummy class for type checking
beforeEach(async () => {
mockRegistrarService = {
registrar: signal<Registrar | null | undefined>(undefined),
registrarId: signal('123'),
registrars: signal([]),
};
mockUserDataService = {
userData: signal({
globalRole: 'NONE',
}),
};
mockSnackBar = jasmine.createSpyObj('MatSnackBar', ['openFromComponent']);
await TestBed.configureTestingModule({
declarations: [AppComponent],
imports: [RouterTestingModule, MaterialModule, BrowserAnimationsModule],
imports: [
MatSidenavModule,
NoopAnimationsModule,
MatSnackBarModule,
AppModule,
RouterModule.forRoot(routes),
],
providers: [
BackendService,
{ provide: RegistrarService, useValue: mockRegistrarService },
{ provide: UserDataService, useValue: mockUserDataService },
{ provide: MatSnackBar, useValue: mockSnackBar },
{ provide: PocReminderComponent, useClass: dummyPocReminderComponent },
provideHttpClient(),
provideHttpClientTesting(),
],
}).compileComponents();
fixture = TestBed.createComponent(AppComponent);
component = fixture.componentInstance;
});
afterEach(() => {
jasmine.clock().uninstall();
});
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
describe('PoC Verification Reminder', () => {
beforeEach(() => {
jasmine.clock().install();
});
it('should open snackbar if lastPocVerificationDate is older than one year', fakeAsync(() => {
const MOCK_TODAY = new Date('2024-07-15T10:00:00.000Z');
jasmine.clock().mockDate(MOCK_TODAY);
const twoYearsAgo = new Date(MOCK_TODAY);
twoYearsAgo.setFullYear(MOCK_TODAY.getFullYear() - 2);
mockRegistrarService.registrar.set({
lastPocVerificationDate: twoYearsAgo.toISOString(),
});
fixture.detectChanges();
TestBed.flushEffects();
expect(mockSnackBar.openFromComponent).toHaveBeenCalledWith(
PocReminderComponent,
{
horizontalPosition: 'center',
verticalPosition: 'top',
duration: 1000000000,
}
);
}));
it('should NOT open snackbar if lastPocVerificationDate is within last year', fakeAsync(() => {
const MOCK_TODAY = new Date('2024-07-15T10:00:00.000Z');
jasmine.clock().mockDate(MOCK_TODAY);
const sixMonthsAgo = new Date(MOCK_TODAY);
sixMonthsAgo.setMonth(MOCK_TODAY.getMonth() - 6);
mockRegistrarService.registrar.set({
lastPocVerificationDate: sixMonthsAgo.toISOString(),
});
fixture.detectChanges();
TestBed.flushEffects();
expect(mockSnackBar.openFromComponent).not.toHaveBeenCalled();
}));
});
});

View File

@@ -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) => {

View File

@@ -26,10 +26,15 @@ import { BackendService } from './shared/services/backend.service';
import { provideHttpClient } from '@angular/common/http';
import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field';
import { BillingInfoComponent } from './billingInfo/billingInfo.component';
import { DomainListComponent } from './domains/domainList.component';
import {
DomainListComponent,
ReasonDialogComponent,
ResponseDialogComponent,
} from './domains/domainList.component';
import { RegistryLockComponent } from './domains/registryLock.component';
import { HeaderComponent } from './header/header.component';
import { HomeComponent } from './home/home.component';
import { RegistryLockVerifyComponent } from './lock/registryLockVerify.component';
import { NavigationComponent } from './navigation/navigation.component';
import NewRegistrarComponent from './registrar/newRegistrar.component';
import { RegistrarDetailsComponent } from './registrar/registrarDetails.component';
@@ -42,8 +47,6 @@ import EppPasswordEditComponent from './settings/security/eppPasswordEdit.compon
import SecurityComponent from './settings/security/security.component';
import SecurityEditComponent from './settings/security/securityEdit.component';
import { SettingsComponent } from './settings/settings.component';
import WhoisComponent from './settings/whois/whois.component';
import WhoisEditComponent from './settings/whois/whoisEdit.component';
import { NotificationsComponent } from './shared/components/notifications/notifications.component';
import { SelectedRegistrarWrapper } from './shared/components/selectedRegistrarWrapper/selectedRegistrarWrapper.component';
import { LocationBackDirective } from './shared/directives/locationBack.directive';
@@ -53,8 +56,22 @@ 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 { RegistryLockVerifyComponent } from './lock/registryLockVerify.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: [
@@ -63,28 +80,34 @@ import { RegistryLockVerifyComponent } from './lock/registryLockVerify.component
ContactDetailsComponent,
DomainListComponent,
EppPasswordEditComponent,
ForceFocusDirective,
HeaderComponent,
HistoryComponent,
HistoryListComponent,
HomeComponent,
LocationBackDirective,
UserLevelVisibility,
NavigationComponent,
NewRegistrarComponent,
NotificationsComponent,
PasswordInputForm,
PasswordResetVerifyComponent,
PocReminderComponent,
RdapComponent,
RdapEditComponent,
ReasonDialogComponent,
RegistrarComponent,
RegistrarDetailsComponent,
RegistryLockComponent,
RegistrarSelectorComponent,
RegistryLockComponent,
RegistryLockVerifyComponent,
ResourcesComponent,
ResponseDialogComponent,
SecurityComponent,
SecurityEditComponent,
SelectedRegistrarWrapper,
SettingsComponent,
SettingsContactComponent,
SupportComponent,
TldsComponent,
WhoisComponent,
WhoisEditComponent,
UserLevelVisibility,
],
bootstrap: [AppComponent],
imports: [
@@ -93,6 +116,7 @@ import { RegistryLockVerifyComponent } from './lock/registryLockVerify.component
BrowserModule,
FormsModule,
MaterialModule,
SelectedRegistrarModule,
SnackBarModule,
],
providers: [

View File

@@ -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>

View File

@@ -16,7 +16,7 @@
width: 100%;
}
&-subhead {
font-size: 20px;
font-size: 1.25rem;
margin-bottom: 20px;
}
}

View File

@@ -20,6 +20,7 @@ import { MatSnackBar } from '@angular/material/snack-bar';
selector: 'app-billingInfo',
templateUrl: './billingInfo.component.html',
styleUrls: ['./billingInfo.component.scss'],
standalone: false,
})
export class BillingInfoComponent {
public static PATH = 'billingInfo';

View File

@@ -1,6 +1,6 @@
<app-selected-registrar-wrapper>
<div class="console-app-domains">
<h1 class="mat-headline-4">Domains</h1>
<h1 class="mat-headline-4" forceFocus>Domains</h1>
<div
class="console-app-domains__actions-wrapper"
@@ -24,11 +24,37 @@
</div>
} @else {
<mat-menu #actions="matMenu">
<ng-template matMenuContent let-domainName="domainName">
<button mat-menu-item (click)="openRegistryLock(domainName)">
<ng-template
matMenuContent
let-domainName="domainName"
let-domain="domain"
>
<button
mat-menu-item
(click)="openRegistryLock(domainName)"
aria-label="Access registry lock for domain"
>
<mat-icon>key</mat-icon>
<span>Registry Lock</span>
</button>
<button
mat-menu-item
(click)="onSuspendClick(domainName)"
[elementId]="getElementIdForSuspendUnsuspend()"
[disabled]="isDomainUnsuspendable(domain)"
>
<mat-icon>lock_clock</mat-icon>
<span>Suspend</span>
</button>
<button
mat-menu-item
(click)="onUnsuspendClick(domainName)"
[elementId]="getElementIdForSuspendUnsuspend()"
[disabled]="!isDomainUnsuspendable(domain)"
>
<mat-icon>lock_open</mat-icon>
<span>Unsuspend</span>
</button>
</ng-template>
</mat-menu>
<div
@@ -65,16 +91,67 @@
/>
</mat-form-field>
<div
class="console-app__domains-selection"
[elementId]="getElementIdForBulkDelete()"
[ngClass]="{ active: selection.hasValue() }"
>
<div class="console-app__domains-selection-text">
{{ selection.selected.length }} Selected
</div>
<div class="console-app__domains-selection-actions">
<button
mat-flat-button
aria-label="Delete Selected Domains"
[attr.aria-hidden]="!selection.hasValue()"
(click)="deleteSelectedDomains()"
>
Delete Selected Domains
</button>
</div>
</div>
<mat-table
[dataSource]="dataSource"
class="mat-elevation-z0"
class="console-app__domains-table"
>
<!-- Checkbox Column -->
<ng-container matColumnDef="select">
<mat-header-cell *matHeaderCellDef>
<mat-checkbox
(change)="$event ? toggleAllRows() : null"
[checked]="selection.hasValue() && isAllSelected"
[indeterminate]="selection.hasValue() && !isAllSelected"
[aria-label]="checkboxLabel()"
[elementId]="getElementIdForBulkDelete()"
>
</mat-checkbox>
</mat-header-cell>
<mat-cell *matCellDef="let row">
<mat-checkbox
(click)="$event.stopPropagation()"
(change)="$event ? selection.toggle(row) : null"
[checked]="selection.isSelected(row)"
[aria-label]="checkboxLabel(row)"
[elementId]="getElementIdForBulkDelete()"
>
</mat-checkbox>
</mat-cell>
</ng-container>
<ng-container matColumnDef="domainName">
<mat-header-cell *matHeaderCellDef>Domain Name</mat-header-cell>
<mat-cell *matCellDef="let element">{{
element.domainName
}}</mat-cell>
<mat-cell *matCellDef="let element">
<mat-icon
*ngIf="getOperationMessage(element.domainName)"
[matTooltip]="getOperationMessage(element.domainName)"
matTooltipPosition="above"
class="primary-text"
>info</mat-icon
>
<span>{{ element.domainName }}</span>
</mat-cell>
</ng-container>
<ng-container matColumnDef="creationTime">
@@ -95,9 +172,9 @@
<ng-container matColumnDef="statuses">
<mat-header-cell *matHeaderCellDef>Statuses</mat-header-cell>
<mat-cell *matCellDef="let element">{{
element.statuses
}}</mat-cell>
<mat-cell *matCellDef="let element">
<span>{{ element.statuses?.join(", ") }}</span>
</mat-cell>
</ng-container>
<ng-container matColumnDef="registryLock">
@@ -115,7 +192,10 @@
<button
mat-icon-button
[matMenuTriggerFor]="actions"
[matMenuTriggerData]="{ domainName: element.domainName }"
[matMenuTriggerData]="{
domainName: element.domainName,
domain: element
}"
aria-label="Domain actions"
>
<mat-icon>more_horiz</mat-icon>

View File

@@ -12,6 +12,22 @@
}
}
&__domains-selection {
height: 60px;
max-height: 0;
transition: max-height 0.2s linear;
display: flex;
align-items: center;
overflow: hidden;
gap: 20px;
&-text {
font-weight: bold;
}
&.active {
max-height: 60px;
}
}
&-domains__download {
position: absolute;
top: -55px;
@@ -36,6 +52,27 @@
.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 {

View File

@@ -20,6 +20,8 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MaterialModule } from '../material.module';
import { BackendService } from '../shared/services/backend.service';
import { DomainListComponent } from './domainList.component';
import { FormsModule } from '@angular/forms';
import { AppModule } from '../app.module';
describe('DomainListComponent', () => {
let component: DomainListComponent;
@@ -28,7 +30,12 @@ describe('DomainListComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [DomainListComponent],
imports: [MaterialModule, BrowserAnimationsModule],
imports: [
MaterialModule,
BrowserAnimationsModule,
FormsModule,
AppModule,
],
providers: [
BackendService,
provideHttpClient(),

View File

@@ -12,27 +12,123 @@
// 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 { Domain, DomainListService } from './domainList.service';
import {
BULK_ACTION_NAME,
Domain,
DomainListService,
} from './domainList.service';
import { RegistryLockComponent } from './registryLock.component';
import { RegistryLockService } from './registryLock.service';
import {
MAT_DIALOG_DATA,
MatDialog,
MatDialogRef,
} from '@angular/material/dialog';
import { RESTRICTED_ELEMENTS } from '../shared/directives/userLevelVisiblity.directive';
interface DomainResponse {
message: string;
responseCode: string;
}
interface DomainData {
[domain: string]: DomainResponse;
}
@Component({
selector: 'app-response-dialog',
template: `
<h2 mat-dialog-title>{{ data.title }}</h2>
<mat-dialog-content [innerHTML]="data.content" />
<mat-dialog-actions>
<button mat-button (click)="onClose()">Close</button>
</mat-dialog-actions>
`,
standalone: false,
})
export class ResponseDialogComponent {
constructor(
public dialogRef: MatDialogRef<ReasonDialogComponent>,
@Inject(MAT_DIALOG_DATA)
public data: { title: string; content: string }
) {}
onClose(): void {
this.dialogRef.close();
}
}
enum Operation {
deleting = 'deleting',
suspending = 'suspending',
unsuspending = 'unsuspending',
}
@Component({
selector: 'app-reason-dialog',
template: `
<h2 mat-dialog-title>
Please provide the (EPP) reason for {{ data.operation }} the domain(s):
</h2>
<mat-dialog-content>
<mat-form-field appearance="outline" style="width:100%">
<textarea matInput [(ngModel)]="reason" rows="4"></textarea>
</mat-form-field>
</mat-dialog-content>
<mat-dialog-actions>
<button mat-button (click)="onCancel()">Cancel</button>
<button mat-button color="warn" (click)="onSave()" [disabled]="!reason">
Save
</button>
</mat-dialog-actions>
`,
standalone: false,
})
export class ReasonDialogComponent {
reason: string = '';
constructor(
public dialogRef: MatDialogRef<ReasonDialogComponent>,
@Inject(MAT_DIALOG_DATA)
public data: { operation: Operation }
) {}
onSave(): void {
this.dialogRef.close(this.reason);
}
onCancel(): void {
this.dialogRef.close();
}
}
@Component({
selector: 'app-domain-list',
templateUrl: './domainList.component.html',
styleUrls: ['./domainList.component.scss'],
standalone: false,
})
export class DomainListComponent {
public static PATH = 'domain-list';
private static SUSPENDED_STATUSES = [
'SERVER_RENEW_PROHIBITED',
'SERVER_TRANSFER_PROHIBITED',
'SERVER_UPDATE_PROHIBITED',
'SERVER_DELETE_PROHIBITED',
'SERVER_HOLD',
];
private readonly DEBOUNCE_MS = 500;
isAllSelected = false;
displayedColumns: string[] = [
'select',
'domainName',
'creationTime',
'registrationExpirationTime',
@@ -42,6 +138,7 @@ export class DomainListComponent {
];
dataSource: MatTableDataSource<Domain> = new MatTableDataSource();
selection = new SelectionModel<Domain>(true, [], undefined, this.isChecked());
isLoading = true;
searchTermSubject = new Subject<string>();
@@ -51,13 +148,18 @@ export class DomainListComponent {
resultsPerPage = 50;
totalResults?: number = 0;
reason: string = '';
operationResult: DomainData | undefined;
@ViewChild(MatPaginator, { static: true }) paginator!: MatPaginator;
constructor(
protected domainListService: DomainListService,
protected registrarService: RegistrarService,
protected registryLockService: RegistryLockService,
private _snackBar: MatSnackBar
private _snackBar: MatSnackBar,
private dialog: MatDialog
) {
effect(() => {
this.pageNumber = 0;
@@ -91,7 +193,10 @@ export class DomainListComponent {
loadLocks() {
this.registryLockService.retrieveLocks().subscribe({
error: (err: HttpErrorResponse) => {
this._snackBar.open(err.message);
if (err.status !== HttpStatusCode.Forbidden) {
// Some users may not have registry lock permissions and that's OK
this._snackBar.open(err.message);
}
},
});
}
@@ -131,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
);
});
}
}

View File

@@ -35,6 +35,12 @@ export interface DomainListResult {
totalResults: number;
}
export enum BULK_ACTION_NAME {
DELETE = 'DELETE',
SUSPEND = 'SUSPEND',
UNSUSPEND = 'UNSUSPEND',
}
@Injectable({
providedIn: 'root',
})
@@ -48,7 +54,6 @@ export class DomainListService {
private backendService: BackendService,
private registrarService: RegistrarService
) {}
retrieveDomains(
pageNumber?: number,
resultsPerPage?: number,
@@ -71,4 +76,18 @@ export class DomainListService {
})
);
}
bulkDomainAction(
domains: string[],
reason: string,
registrarId: string,
actionName: BULK_ACTION_NAME
) {
return this.backendService.bulkDomainAction(
domains,
reason,
actionName,
registrarId
);
}
}

View File

@@ -49,6 +49,7 @@
color="primary"
type="submit"
[disabled]="!unlockDomain.valid"
aria-label="Submit domain unlock request"
>
Save
</button>
@@ -73,6 +74,7 @@
color="primary"
type="submit"
[disabled]="!lockDomain.valid"
aria-label="Submit domain lock request"
>
Save
</button>

View File

@@ -25,6 +25,7 @@ import { RegistryLockService } from './registryLock.service';
selector: 'app-registry-lock',
templateUrl: './registryLock.component.html',
styleUrls: ['./registryLock.component.scss'],
standalone: false,
})
export class RegistryLockComponent {
readonly isLocked = computed(() =>

View File

@@ -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

View File

@@ -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();

View File

@@ -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;

View 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>

View File

@@ -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.backend;
.history-log {
font-family: "Roboto", sans-serif;
max-width: 760px;
.spacer {
margin: 20px 0;
}
}

View 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;
},
});
}
}

View 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);
}
})
);
}
}

View 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>
}

View 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;
}
}

View 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,
};
}
}

View File

@@ -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();

View File

@@ -25,6 +25,7 @@ import { BreakPointObserverService } from '../shared/services/breakPoint.service
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.scss'],
standalone: false,
})
export class HomeComponent {
constructor(

View File

@@ -3,7 +3,7 @@
margin-top: 30px;
}
&-subhead {
font-size: 20px;
font-size: 1.25rem;
margin-bottom: 20px;
}
}

View File

@@ -25,6 +25,7 @@ import { DomainListComponent } from '../domains/domainList.component';
templateUrl: './registryLockVerify.component.html',
styleUrls: ['./registryLockVerify.component.scss'],
providers: [RegistryLockVerifyService],
standalone: false,
})
export class RegistryLockVerifyComponent {
public static PATH = 'registry-lock-verify';

View File

@@ -6,7 +6,9 @@
<mat-tree-node
*matTreeNodeDef="let node"
matTreeNodeToggle
tabindex="0"
(click)="onClick(node)"
(keyup.enter)="onClick(node)"
[class.active]="router.url.includes(node.path)"
[elementId]="getElementId(node)"
>
@@ -18,6 +20,8 @@
<mat-nested-tree-node
*matTreeNodeDef="let node; when: hasChild"
(click)="onClick(node)"
tabindex="0"
(keyup.enter)="onClick(node)"
>
<div class="mat-tree-node" [class.active]="router.url.includes(node.path)">
<button

View File

@@ -17,8 +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;
@@ -32,6 +33,7 @@ interface NavMenuNode extends RouteWithIcon {
selector: 'app-navigation',
templateUrl: './navigation.component.html',
styleUrls: ['./navigation.component.scss'],
standalone: false,
})
export class NavigationComponent {
renderRouter: boolean = true;
@@ -55,13 +57,16 @@ export class NavigationComponent {
}
ngOnDestroy() {
this.subscription.unsubscribe();
this.subscription && this.subscription.unsubscribe();
}
getElementId(node: RouteWithIcon) {
return node.path === 'registrars'
? RESTRICTED_ELEMENTS.REGISTRAR_ELEMENT
: null;
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) {

View 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>

View 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;
}
}

View 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);
},
});
}
}
}

View 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>
}

View 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;
}
}
}

View 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);
},
});
});
}
}

View File

@@ -175,6 +175,7 @@ JPY=billing-id-for-yen"
mat-flat-button
color="primary"
type="submit"
aria-label="Submit new registrar request"
>
Save
</button>

View File

@@ -33,6 +33,7 @@ interface LocalizedAddressStreet {
templateUrl: './newRegistrar.component.html',
styleUrls: ['./newRegistrar.component.scss'],
encapsulation: ViewEncapsulation.None,
standalone: false,
})
export default class NewRegistrarComponent {
protected newRegistrar: Registrar;
@@ -47,7 +48,6 @@ export default class NewRegistrarComponent {
this.newRegistrar = {
registrarId: '',
url: '',
whoisServer: '',
registrarName: '',
icannReferralEmail: '',
localizedAddress: {

View File

@@ -17,6 +17,8 @@ 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({
@@ -149,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);
}
}

View File

@@ -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"

View File

@@ -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;
@@ -70,6 +74,16 @@ export class RegistrarDetailsComponent implements OnInit {
];
}
checkOteStatus() {
this.router.navigate(['ote-status/', this.registrarInEdit.registrarId], {
queryParamsHandling: 'merge',
});
}
getElementIdForOteBlock() {
return RESTRICTED_ELEMENTS.OTE;
}
removeTLD(tld: string) {
this.registrarInEdit.allowedTlds = this.registrarInEdit.allowedTlds?.filter(
(v) => v != tld
@@ -91,6 +105,6 @@ export class RegistrarDetailsComponent implements OnInit {
}
ngOnDestroy() {
this.subscription.unsubscribe();
this.subscription && this.subscription.unsubscribe();
}
}

View File

@@ -11,6 +11,7 @@
[ngModelOptions]="{ standalone: true }"
(focus)="onFocus()"
[matAutocomplete]="auto"
spellcheck="false"
/>
<mat-autocomplete
autoActiveFirstOption

View File

@@ -20,6 +20,7 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MaterialModule } from '../material.module';
import { BackendService } from '../shared/services/backend.service';
import { RegistrarSelectorComponent } from './registrarSelector.component';
import { FormsModule } from '@angular/forms';
describe('RegistrarSelectorComponent', () => {
let component: RegistrarSelectorComponent;
@@ -28,7 +29,7 @@ describe('RegistrarSelectorComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [RegistrarSelectorComponent],
imports: [MaterialModule, BrowserAnimationsModule],
imports: [MaterialModule, BrowserAnimationsModule, FormsModule],
providers: [
BackendService,
provideHttpClient(),

View File

@@ -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) {

View File

@@ -3,8 +3,19 @@
} @else {
<div class="console-app__registrars">
<div class="console-app__registrars-header">
<h1 class="mat-headline-4">Registrars</h1>
<h1 class="mat-headline-4" forceFocus>Registrars</h1>
<div class="spacer"></div>
<button
mat-stroked-button
*ngIf="oteButtonVisible"
(click)="createOteAccount()"
aria-label="Generate OT&E accounts"
[elementId]="getElementIdForOteBlock()"
>
Create OT&E accounts
</button>
<button
class="console-app__registrars-new"
mat-flat-button
color="primary"
(click)="openNewRegistrar()"
@@ -48,6 +59,8 @@
<mat-row
*matRowDef="let row; columns: displayedColumns"
(click)="openDetails(row.registrarId)"
tabindex="0"
(keyup.enter)="openDetails(row.registrarId)"
></mat-row>
</mat-table>

View File

@@ -10,6 +10,10 @@
min-width: $min-width !important;
}
&__registrars-new {
margin-left: 20px;
}
&__registrars-header {
display: flex;
justify-content: space-between;

View File

@@ -17,7 +17,10 @@ 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 = [
{
@@ -75,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;
@@ -103,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',

View File

@@ -1,4 +1,4 @@
<h1 class="mat-headline-4">Resources</h1>
<h1 class="mat-headline-4" forceFocus>Resources</h1>
<div class="console-app__resources">
<div>
<div class="console-app__resources-subhead">Technical resources</div>
@@ -11,6 +11,6 @@
>
</div>
<div>
<img src="./assets/resources.png" />
<img src="./assets/resources.png" alt="Generic resources image" />
</div>
</div>

View File

@@ -16,7 +16,7 @@
width: 100%;
}
&-subhead {
font-size: 20px;
font-size: 1.25rem;
margin-bottom: 20px;
}
}

View File

@@ -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';

View File

@@ -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>
}

View File

@@ -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';

View File

@@ -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()));
}
}

View File

@@ -1,4 +1,9 @@
<div class="console-app__contact" *ngIf="contactService.contactInEdit">
<div
class="console-app__contact"
*ngIf="contactService.contactInEdit"
cdkTrapFocus
[cdkTrapFocusAutoCapture]="true"
>
<div class="console-app__contact-controls">
<button
mat-icon-button
@@ -51,6 +56,12 @@
[required]="true"
[(ngModel)]="contactService.contactInEdit.emailAddress"
[ngModelOptions]="{ standalone: true }"
[disabled]="emailAddressIsDisabled()"
[matTooltip]="
emailAddressIsDisabled()
? 'Reach out to registry customer support to update email address'
: ''
"
/>
</mat-form-field>
@@ -78,43 +89,48 @@
<h1>Contact Type</h1>
<p class="console-app__contact-required">
<mat-icon color="accent">error</mat-icon>Required to select at least one
(primary contact can't be updated)
</p>
<div class="">
<mat-checkbox
<ng-container
*ngFor="let contactType of contactTypeToTextMap | keyvalue"
[checked]="checkboxIsChecked(contactType.key)"
(change)="checkboxOnChange($event, contactType.key)"
[disabled]="checkboxIsDisabled(contactType.key)"
>
{{ contactType.value }}
</mat-checkbox>
<mat-checkbox
*ngIf="shouldDisplayCheckbox(contactType.key)"
[checked]="checkboxIsChecked(contactType.key)"
(change)="checkboxOnChange($event, contactType.key)"
[disabled]="checkboxIsDisabled(contactType.key)"
>
{{ contactType.value }}
</mat-checkbox>
</ng-container>
</div>
</section>
<section>
<h1>WHOIS Preferences</h1>
<h1>RDAP Preferences</h1>
<div>
<mat-checkbox
[(ngModel)]="contactService.contactInEdit.visibleInWhoisAsAdmin"
[(ngModel)]="contactService.contactInEdit.visibleInRdapAsAdmin"
[ngModelOptions]="{ standalone: true }"
>Show in Registrar WHOIS record as admin contact</mat-checkbox
>Show in Registrar RDAP record as admin contact</mat-checkbox
>
</div>
<div>
<mat-checkbox
[(ngModel)]="contactService.contactInEdit.visibleInWhoisAsTech"
[(ngModel)]="contactService.contactInEdit.visibleInRdapAsTech"
[ngModelOptions]="{ standalone: true }"
>Show in Registrar WHOIS record as technical contact</mat-checkbox
>Show in Registrar RDAP record as technical contact</mat-checkbox
>
</div>
<div>
<mat-checkbox
[(ngModel)]="contactService.contactInEdit.visibleInDomainWhoisAsAbuse"
[(ngModel)]="contactService.contactInEdit.visibleInDomainRdapAsAbuse"
[ngModelOptions]="{ standalone: true }"
>Show Phone and Email in Domain WHOIS Record as registrar abuse
contact (per CL&D requirements)</mat-checkbox
>Show Phone and Email in Domain RDAP Record as registrar abuse contact
(per CL&D requirements)</mat-checkbox
>
</div>
</section>
@@ -123,6 +139,7 @@
mat-flat-button
color="primary"
type="submit"
aria-label="Save contact updates"
>
Save
</button>
@@ -170,30 +187,30 @@
<mat-card-content>
<mat-list role="list">
<mat-list-item role="listitem">
<h2>WHOIS Preferences</h2>
<h2>RDAP Preferences</h2>
</mat-list-item>
@if(contactService.contactInEdit.visibleInWhoisAsAdmin) {
@if(contactService.contactInEdit.visibleInRdapAsAdmin) {
<mat-divider></mat-divider>
<mat-list-item role="listitem">
<span class="console-app__list-value"
>Show in Registrar WHOIS record as admin contact</span
>Show in Registrar RDAP record as admin contact</span
>
</mat-list-item>
} @if(contactService.contactInEdit.visibleInWhoisAsTech) {
} @if(contactService.contactInEdit.visibleInRdapAsTech) {
<mat-divider></mat-divider>
<mat-list-item
role="listitem"
*ngIf="contactService.contactInEdit.visibleInWhoisAsTech"
*ngIf="contactService.contactInEdit.visibleInRdapAsTech"
>
<span class="console-app__list-value"
>Show in Registrar WHOIS record as technical contact</span
>Show in Registrar RDAP record as technical contact</span
>
</mat-list-item>
} @if(contactService.contactInEdit.visibleInDomainWhoisAsAbuse) {
} @if(contactService.contactInEdit.visibleInDomainRdapAsAbuse) {
<mat-divider></mat-divider>
<mat-list-item role="listitem">
<span class="console-app__list-value"
>Show Phone and Email in Domain WHOIS Record as registrar abuse
>Show Phone and Email in Domain RDAP Record as registrar abuse
contact (per CL&D requirements)</span
>
</mat-list-item>

View File

@@ -27,6 +27,7 @@ import {
selector: 'app-contact-details',
templateUrl: './contactDetails.component.html',
styleUrls: ['./contactDetails.component.scss'],
standalone: false,
})
export class ContactDetailsComponent {
protected contactTypeToTextMap = contactTypeToTextMap;
@@ -68,9 +69,13 @@ export class ContactDetailsComponent {
save(e: SubmitEvent) {
e.preventDefault();
if ((this.contactService.contactInEdit.types || []).length === 0) {
this._snackBar.open('Required to select contact type');
return;
}
const request = this.contactService.isContactNewView
? this.contactService.addContact(this.contactService.contactInEdit)
: this.contactService.saveContacts(this.contactService.contacts());
: this.contactService.updateContact(this.contactService.contactInEdit);
request.subscribe({
complete: () => {
this.goBack();
@@ -81,6 +86,10 @@ export class ContactDetailsComponent {
});
}
shouldDisplayCheckbox(type: string) {
return type !== 'ADMIN' || this.checkboxIsChecked(type);
}
checkboxIsChecked(type: string) {
return this.contactService.contactInEdit.types.includes(
type as contactType
@@ -88,6 +97,9 @@ export class ContactDetailsComponent {
}
checkboxIsDisabled(type: string) {
if (type === 'ADMIN') {
return true;
}
return (
this.contactService.contactInEdit.types.length === 1 &&
this.contactService.contactInEdit.types[0] === (type as contactType)
@@ -104,4 +116,8 @@ export class ContactDetailsComponent {
);
}
}
emailAddressIsDisabled() {
return this.contactService.contactInEdit.types.includes('ADMIN');
}
}

View File

@@ -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>
}

View File

@@ -1,4 +1,4 @@
.console-app__whois {
.console-app__rdap {
max-width: 616px;
&-controls {

View File

@@ -20,25 +20,32 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MaterialModule } from 'src/app/material.module';
import { RegistrarService } from 'src/app/registrar/registrar.service';
import { BackendService } from 'src/app/shared/services/backend.service';
import WhoisComponent from './whois.component';
import RdapComponent from './rdap.component';
describe('WhoisComponent', () => {
let component: WhoisComponent;
let fixture: ComponentFixture<WhoisComponent>;
describe('RdapComponent', () => {
let component: RdapComponent;
let fixture: ComponentFixture<RdapComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [WhoisComponent],
declarations: [RdapComponent],
imports: [MaterialModule, BrowserAnimationsModule],
providers: [
BackendService,
{ 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();
});

View File

@@ -14,16 +14,16 @@
import { Component, computed } from '@angular/core';
import { RegistrarService } from 'src/app/registrar/registrar.service';
import { WhoisService } from './whois.service';
import { RdapService } from './rdap.service';
@Component({
selector: 'app-whois',
templateUrl: './whois.component.html',
styleUrls: ['./whois.component.scss'],
selector: 'app-rdap',
templateUrl: './rdap.component.html',
styleUrls: ['./rdap.component.scss'],
standalone: false,
})
export default class WhoisComponent {
public static PATH = 'whois';
export default class RdapComponent {
public static PATH = 'rdap';
formattedAddress = computed(() => {
let result = '';
const registrar = this.registrarService.registrar();
@@ -46,7 +46,7 @@ export default class WhoisComponent {
});
constructor(
public whoisService: WhoisService,
public rdapService: RdapService,
public registrarService: RegistrarService
) {}
}

View File

@@ -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();
})

View File

@@ -1,22 +1,27 @@
<div class="console-app__whois-edit" *ngIf="registrarInEdit">
<div
class="console-app__rdap-edit"
*ngIf="registrarInEdit"
cdkTrapFocus
[cdkTrapFocusAutoCapture]="true"
>
<button
mat-icon-button
class="console-app__whois-edit-back"
aria-label="Back to whois view"
(click)="whoisService.editing = false"
class="console-app__rdap-edit-back"
aria-label="Back to rdap view"
(click)="rdapService.editing = false"
>
<mat-icon>arrow_back</mat-icon>
</button>
<div class="console-app__whois-edit-controls">
<div class="console-app__rdap-edit-controls">
<span>
General registrar information for your WHOIS record. This information is
always visible in WHOIS.
General registrar information for your RDAP record. This information is
always visible in RDAP.
</span>
<div class="spacer"></div>
</div>
<div class="console-app__whois-edit">
<div class="console-app__rdap-edit">
<h1>Personal info</h1>
<form (ngSubmit)="save($event)">
@@ -110,41 +115,14 @@
/>
</mat-form-field>
<h1>Technical info</h1>
<mat-form-field appearance="outline">
<mat-label>WHOIS server: </mat-label>
<input
matInput
type="text"
[(ngModel)]="registrarInEdit.whoisServer"
[ngModelOptions]="{ standalone: true }"
/>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Referral URL: </mat-label>
<input
matInput
type="text"
[(ngModel)]="registrarInEdit.url"
[ngModelOptions]="{ standalone: true }"
/>
</mat-form-field>
@if((userDataService.userData()?.globalRole || 'NONE') !== "NONE") {
<mat-form-field appearance="outline">
<mat-label>ICANN Referral Email: </mat-label>
<input
matInput
type="text"
[(ngModel)]="registrarInEdit.icannReferralEmail"
[ngModelOptions]="{ standalone: true }"
/>
</mat-form-field>
}
<button mat-flat-button color="primary" type="submit">Save</button>
<button
mat-flat-button
color="primary"
type="submit"
aria-label="Save RDAO settings"
>
Save
</button>
</form>
</div>
</div>

View File

@@ -1,4 +1,4 @@
.console-app__whois-edit {
.console-app__rdap-edit {
max-width: 616px;
&-controls {

View File

@@ -20,19 +20,20 @@ import {
RegistrarService,
} from 'src/app/registrar/registrar.service';
import { UserDataService } from 'src/app/shared/services/userData.service';
import { WhoisService } from './whois.service';
import { RdapService } from './rdap.service';
@Component({
selector: 'app-whois-edit',
templateUrl: './whoisEdit.component.html',
styleUrls: ['./whoisEdit.component.scss'],
selector: 'app-rdap-edit',
templateUrl: './rdapEdit.component.html',
styleUrls: ['./rdapEdit.component.scss'],
standalone: false,
})
export default class WhoisEditComponent {
export default class RdapEditComponent {
registrarInEdit: Registrar | undefined;
constructor(
public userDataService: UserDataService,
public whoisService: WhoisService,
public rdapService: RdapService,
public registrarService: RegistrarService,
private _snackBar: MatSnackBar
) {
@@ -48,9 +49,9 @@ export default class WhoisEditComponent {
e.preventDefault();
if (!this.registrarInEdit) return;
this.whoisService.saveChanges(this.registrarInEdit).subscribe({
this.rdapService.saveChanges(this.registrarInEdit).subscribe({
complete: () => {
this.whoisService.editing = false;
this.rdapService.editing = false;
},
error: (err: HttpErrorResponse) => {
this._snackBar.open(err.error);

View File

@@ -1,4 +1,8 @@
<div class="settings-security__edit-password">
<div
class="settings-security__edit-password"
cdkTrapFocus
[cdkTrapFocusAutoCapture]="true"
>
<p>
<button
mat-icon-button
@@ -12,65 +16,22 @@
<p class="secondary-text">
Passwords must be between 6 and 16 alphanumeric characters
</p>
<form
(ngSubmit)="save()"
<password-input-form-component
[displayOldPasswordField]="true"
[formGroup]="passwordUpdateForm"
class="settings-security__edit-password-form"
>
<div class="settings-security__edit-password-field">
<mat-form-field appearance="outline">
<mat-label>Old password: </mat-label>
<input
matInput
type="text"
formControlName="oldPassword"
required
autocomplete="current-password"
/>
<mat-error *ngIf="hasError('oldPassword') as errorText">{{
errorText
}}</mat-error>
</mat-form-field>
</div>
<div class="settings-security__edit-password-field">
<mat-form-field appearance="outline">
<mat-label>New password: </mat-label>
<input
matInput
type="text"
formControlName="newPassword"
required
autocomplete="new-password"
/>
<mat-error *ngIf="hasError('newPassword') as errorText">{{
errorText
}}</mat-error>
</mat-form-field>
</div>
<div class="settings-security__edit-password-field">
<mat-form-field appearance="outline">
<mat-label>Confirm new password: </mat-label>
<input
matInput
type="text"
formControlName="newPasswordRepeat"
required
autocomplete="new-password"
/>
<mat-error *ngIf="hasError('newPasswordRepeat') as errorText">{{
errorText
}}</mat-error>
</mat-form-field>
</div>
(submitResults)="save($event)"
/>
@if(userDataService.userData()?.isAdmin) {
<div class="settings-security__reset-password-field">
<h2>Need to reset your EPP password?</h2>
<button
mat-flat-button
color="primary"
[disabled]="!passwordUpdateForm.valid"
aria-label="Save epp password update"
type="submit"
class="settings-security__edit-password-save"
aria-label="Reset EPP password via email"
(click)="requestEppPasswordReset()"
>
Save
Reset EPP password via email
</button>
</form>
</div>
}
</div>

View File

@@ -1,16 +1,19 @@
.settings-security__edit-password {
max-width: 616px;
&-field {
width: 100%;
mat-form-field {
margin-bottom: 20px;
width: 100%;
}
}
&-form {
margin-top: 30px;
}
&-save {
margin-top: 30px;
// Copyright 2025 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
.settings-security {
&__reset-password-field {
margin-top: 60px;
}
}

View File

@@ -14,97 +14,86 @@
import { HttpErrorResponse } from '@angular/common/http';
import { Component } from '@angular/core';
import {
AbstractControl,
FormControl,
FormGroup,
ValidatorFn,
Validators,
} from '@angular/forms';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { MatSnackBar } from '@angular/material/snack-bar';
import { RegistrarService } from 'src/app/registrar/registrar.service';
import { SecurityService } from './security.service';
import { UserDataService } from 'src/app/shared/services/userData.service';
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
import { CommonModule } from '@angular/common';
import { MaterialModule } from 'src/app/material.module';
import { filter, switchMap, take } from 'rxjs';
import { BackendService } from 'src/app/shared/services/backend.service';
import {
PasswordInputForm,
PasswordResults,
} from 'src/app/shared/components/passwordReset/passwordInputForm.component';
type errorCode = 'required' | 'maxlength' | 'minlength' | 'passwordsDontMatch';
@Component({
selector: 'app-reset-epp-password-dialog',
template: `
<h2 mat-dialog-title>Please confirm the password reset:</h2>
<mat-dialog-content>
This will send an EPP password reset email to the admin POC.
</mat-dialog-content>
<mat-dialog-actions>
<button mat-button (click)="onCancel()">Cancel</button>
<button mat-button color="warn" (click)="onSave()">Confirm</button>
</mat-dialog-actions>
`,
imports: [CommonModule, MaterialModule],
})
export class ResetEppPasswordComponent {
constructor(public dialogRef: MatDialogRef<ResetEppPasswordComponent>) {}
type errorFriendlyText = { [type in errorCode]: String };
onSave(): void {
this.dialogRef.close(true);
}
onCancel(): void {
this.dialogRef.close(false);
}
}
@Component({
selector: 'app-epp-password-edit',
templateUrl: './eppPasswordEdit.component.html',
styleUrls: ['./eppPasswordEdit.component.scss'],
standalone: false,
})
export default class EppPasswordEditComponent {
MIN_MAX_LENGHT = new String(
'Passwords must be between 6 and 16 alphanumeric characters'
);
errorTextMap: errorFriendlyText = {
required: "This field can't be empty",
maxlength: this.MIN_MAX_LENGHT,
minlength: this.MIN_MAX_LENGHT,
passwordsDontMatch: "Passwords don't match",
};
constructor(
public securityService: SecurityService,
private _snackBar: MatSnackBar,
public registrarService: RegistrarService
) {}
hasError(controlName: string) {
const maybeErrors = this.passwordUpdateForm.get(controlName)?.errors;
const maybeError =
maybeErrors && (Object.keys(maybeErrors)[0] as errorCode);
if (maybeError) {
return this.errorTextMap[maybeError];
}
return '';
}
newPasswordsMatch: ValidatorFn = (control: AbstractControl) => {
if (
this.passwordUpdateForm?.get('newPassword')?.value ===
this.passwordUpdateForm?.get('newPasswordRepeat')?.value
) {
this.passwordUpdateForm?.get('newPasswordRepeat')?.setErrors(null);
} else {
// latest angular just won't detect the error without setTimeout
setTimeout(() => {
this.passwordUpdateForm
?.get('newPasswordRepeat')
?.setErrors({ passwordsDontMatch: control.value });
});
}
return null;
};
static EPP_VALIDATORS = [
Validators.required,
Validators.minLength(6),
Validators.maxLength(16),
PasswordInputForm.newPasswordsMatch,
];
passwordUpdateForm = new FormGroup({
oldPassword: new FormControl('', [Validators.required]),
newPassword: new FormControl('', [
Validators.required,
Validators.minLength(6),
Validators.maxLength(16),
this.newPasswordsMatch,
]),
newPasswordRepeat: new FormControl('', [
Validators.required,
Validators.minLength(6),
Validators.maxLength(16),
this.newPasswordsMatch,
]),
newPassword: new FormControl('', EppPasswordEditComponent.EPP_VALIDATORS),
newPasswordRepeat: new FormControl(
'',
EppPasswordEditComponent.EPP_VALIDATORS
),
});
save() {
const { oldPassword, newPassword, newPasswordRepeat } =
this.passwordUpdateForm.value;
if (!oldPassword || !newPassword || !newPasswordRepeat) return;
constructor(
public registrarService: RegistrarService,
public securityService: SecurityService,
protected userDataService: UserDataService,
private backendService: BackendService,
private resetPasswordDialog: MatDialog,
private _snackBar: MatSnackBar
) {}
save(passwordResults: PasswordResults) {
this.securityService
.saveEppPassword({
registrarId: this.registrarService.registrarId(),
oldPassword,
newPassword,
newPasswordRepeat,
oldPassword: passwordResults.oldPassword!,
newPassword: passwordResults.newPassword,
newPasswordRepeat: passwordResults.newPasswordRepeat,
})
.subscribe({
complete: () => {
@@ -119,4 +108,26 @@ export default class EppPasswordEditComponent {
goBack() {
this.securityService.isEditingPassword = false;
}
sendEppPasswordResetRequest() {
return this.backendService.requestEppPasswordReset(
this.registrarService.registrarId()
);
}
requestEppPasswordReset() {
const dialogRef = this.resetPasswordDialog.open(ResetEppPasswordComponent);
dialogRef
.afterClosed()
.pipe(
take(1),
filter((result) => !!result)
)
.pipe(switchMap((_) => this.sendEppPasswordResetRequest()))
.subscribe({
next: (_) => this.goBack(),
error: (err: HttpErrorResponse) =>
this._snackBar.open(err.error || err.message),
});
}
}

View File

@@ -20,20 +20,18 @@ import { FormsModule } from '@angular/forms';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { of } from 'rxjs';
import { MaterialModule } from 'src/app/material.module';
import {
Registrar,
RegistrarService,
} from 'src/app/registrar/registrar.service';
import { RegistrarService } from 'src/app/registrar/registrar.service';
import { BackendService } from 'src/app/shared/services/backend.service';
import SecurityComponent from './security.component';
import { SecurityService, apiToUiConverter } from './security.service';
import { SecurityService } from './security.service';
import SecurityEditComponent from './securityEdit.component';
import { MOCK_REGISTRAR_SERVICE } from 'src/testdata/registrar/registrar.service.mock';
describe('SecurityComponent', () => {
let component: SecurityComponent;
let fixture: ComponentFixture<SecurityComponent>;
let fetchSecurityDetailsSpy: Function;
let saveSpy: Function;
let dummyRegistrarService: RegistrarService;
beforeEach(async () => {
const securityServiceSpy = jasmine.createSpyObj(SecurityService, [
@@ -44,18 +42,15 @@ describe('SecurityComponent', () => {
fetchSecurityDetailsSpy =
securityServiceSpy.fetchSecurityDetails.and.returnValue(of());
saveSpy = securityServiceSpy.saveChanges;
dummyRegistrarService = {
registrar: { ipAddressAllowList: ['123.123.123.123'] },
} as RegistrarService;
saveSpy = securityServiceSpy.saveChanges.and.returnValue(of());
await TestBed.configureTestingModule({
declarations: [SecurityComponent],
declarations: [SecurityEditComponent, SecurityComponent],
imports: [MaterialModule, BrowserAnimationsModule, FormsModule],
providers: [
BackendService,
{ provide: RegistrarService, useValue: dummyRegistrarService },
SecurityService,
{ provide: RegistrarService, useValue: MOCK_REGISTRAR_SERVICE },
provideHttpClient(),
provideHttpClientTesting(),
],
@@ -78,78 +73,65 @@ describe('SecurityComponent', () => {
expect(component).toBeTruthy();
});
it('should render ip allow list', waitForAsync(() => {
component.enableEdit();
it('should render security elements', waitForAsync(() => {
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(
Array.from(
fixture.nativeElement.querySelectorAll(
'.settings-security__ip-allowlist'
)
)
).toHaveSize(1);
expect(
fixture.nativeElement.querySelector('.settings-security__ip-allowlist')
.value
).toBe('123.123.123.123');
let listElems: Array<HTMLElement> = Array.from(
fixture.nativeElement.querySelectorAll('span.console-app__list-value')
);
expect(listElems).toHaveSize(8);
expect(listElems.map((e) => e.textContent)).toEqual([
'Change the password used for EPP logins',
'••••••••••••••',
'Restrict access to EPP production servers to the following IP/IPv6 addresses, or ranges like 1.1.1.0/24',
'123.123.123.123',
'X.509 PEM certificate for EPP production access',
'No client certificate on file.',
'X.509 PEM backup certificate for EPP production access',
'No failover certificate on file.',
]);
});
}));
it('should remove ip', waitForAsync(() => {
expect(
Array.from(
fixture.nativeElement.querySelectorAll(
'.settings-security__ip-allowlist'
)
)
).toHaveSize(1);
component.removeIpEntry(0);
component.dataSource.ipAddressAllowList =
component.dataSource.ipAddressAllowList?.splice(1);
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(
Array.from(
fixture.nativeElement.querySelectorAll(
'.settings-security__ip-allowlist'
)
)
).toHaveSize(0);
let listElems: Array<HTMLElement> = Array.from(
fixture.nativeElement.querySelectorAll('span.console-app__list-value')
);
expect(listElems.map((e) => e.textContent)).toContain(
'No IP addresses on file.'
);
});
}));
it('should toggle inEdit', () => {
expect(component.inEdit).toBeFalse();
component.enableEdit();
expect(component.inEdit).toBeTrue();
it('should toggle isEditingSecurity', () => {
expect(component.securityService.isEditingSecurity).toBeFalse();
component.editSecurity();
expect(component.securityService.isEditingSecurity).toBeTrue();
});
it('should create temporary data structure', () => {
expect(component.dataSource).toEqual(
apiToUiConverter(dummyRegistrarService.registrar)
);
component.removeIpEntry(0);
expect(component.dataSource).toEqual({ ipAddressAllowList: [] });
expect(dummyRegistrarService.registrar).toEqual({
ipAddressAllowList: ['123.123.123.123'],
} as Registrar);
component.cancel();
expect(component.dataSource).toEqual(
apiToUiConverter(dummyRegistrarService.registrar)
);
it('should toggle isEditingPassword', () => {
expect(component.securityService.isEditingPassword).toBeFalse();
component.editEppPassword();
expect(component.securityService.isEditingPassword).toBeTrue();
});
it('should call save', waitForAsync(async () => {
component.enableEdit();
fixture.detectChanges();
component.editSecurity();
await fixture.whenStable();
fixture.detectChanges();
const el = fixture.nativeElement.querySelector(
'.settings-security__clientCertificate'
'.console-app__clientCertificateValue'
);
el.value = 'test';
el.dispatchEvent(new Event('input'));
fixture.detectChanges();
await fixture.whenStable();
fixture.nativeElement
.querySelector('.settings-security__actions-save')
.querySelector('.settings-security__edit-save')
.click();
expect(saveSpy).toHaveBeenCalledOnceWith({
ipAddressAllowList: [{ value: '123.123.123.123' }],

View File

@@ -23,6 +23,7 @@ import { SecurityService, apiToUiConverter } from './security.service';
selector: 'app-security',
templateUrl: './security.component.html',
styleUrls: ['./security.component.scss'],
standalone: false,
})
export default class SecurityComponent {
public static PATH = 'security';
@@ -35,6 +36,7 @@ export default class SecurityComponent {
if (this.registrarService.registrar()) {
this.dataSource = apiToUiConverter(this.registrarService.registrar());
this.securityService.isEditingSecurity = false;
this.securityService.isEditingPassword = false;
}
});
}

View File

@@ -21,11 +21,13 @@ import { BackendService } from 'src/app/shared/services/backend.service';
import SecurityComponent from './security.component';
import {
SecurityService,
SecuritySettings,
SecuritySettingsBackendModel,
apiToUiConverter,
uiToApiConverter,
} from './security.service';
import {
SecuritySettings,
SecuritySettingsBackendModel,
} from 'src/app/registrar/registrar.service';
describe('SecurityService', () => {
const uiMockData: SecuritySettings = {

View File

@@ -1,4 +1,8 @@
<div class="settings-security__edit">
<div
class="settings-security__edit"
cdkTrapFocus
[cdkTrapFocusAutoCapture]="true"
>
<h1>IP Allowlist</h1>
<p>
Restrict access to EPP production servers to the following IP/IPv6
@@ -10,6 +14,7 @@
<mat-form-field appearance="outline">
<input
matInput
[disabled]="isUpdating"
type="text"
[(ngModel)]="ip.value"
[ngModelOptions]="{ standalone: true }"
@@ -18,14 +23,22 @@
<button
matSuffix
mat-icon-button
aria-label="Remove"
[attr.aria-label]="'Remove IP entry ' + ip.value"
(click)="removeIpEntry(ip)"
[disabled]="isUpdating"
>
<mat-icon>close</mat-icon>
</button>
</div>
}
<button mat-button color="primary" (click)="createIpEntry()" type="button">
<button
mat-button
[disabled]="isUpdating"
color="primary"
(click)="createIpEntry()"
aria-label="Add new IP address"
type="button"
>
+ Add IP
</button>
@@ -33,7 +46,9 @@
<p>X.509 PEM certificate for EPP production access.</p>
<mat-form-field appearance="outline">
<textarea
class="console-app__clientCertificateValue"
matInput
[disabled]="isUpdating"
[(ngModel)]="dataSource.clientCertificate"
[ngModelOptions]="{ standalone: true }"
></textarea>
@@ -43,6 +58,7 @@
<mat-form-field appearance="outline">
<textarea
matInput
[disabled]="isUpdating"
[(ngModel)]="dataSource.failoverClientCertificate"
[ngModelOptions]="{ standalone: true }"
></textarea>
@@ -50,6 +66,7 @@
<button
mat-flat-button
color="primary"
[disabled]="isUpdating"
aria-label="Save security settings"
type="submit"
class="settings-security__edit-save"

View File

@@ -26,9 +26,11 @@ import { SecurityService, apiToUiConverter } from './security.service';
selector: 'app-security-edit',
templateUrl: './securityEdit.component.html',
styleUrls: ['./securityEdit.component.scss'],
standalone: false,
})
export default class SecurityEditComponent {
dataSource: SecuritySettings = {};
isUpdating = false;
constructor(
public securityService: SecurityService,
@@ -43,12 +45,15 @@ export default class SecurityEditComponent {
}
save() {
this.isUpdating = true;
this.securityService.saveChanges(this.dataSource).subscribe({
complete: () => {
this.isUpdating = false;
this.goBack();
},
error: (err: HttpErrorResponse) => {
this._snackBar.open(err.error);
this._snackBar.open(err.error || err.message);
this.isUpdating = false;
},
});
}

View File

@@ -1,6 +1,6 @@
<app-selected-registrar-wrapper>
<div class="console-settings">
<h1 class="mat-headline-4">Settings</h1>
<h1 class="mat-headline-4" forceFocus>Settings</h1>
<nav
mat-tab-nav-bar
mat-stretch-tabs="false"
@@ -10,22 +10,31 @@
<a
mat-tab-link
routerLink="contact"
routerLinkActive="active-link"
routerLinkActive
queryParamsHandling="merge"
#rla="routerLinkActive"
[active]="rla.isActive"
aria-label="Access contacts settings"
>Contacts</a
>
<a
mat-tab-link
routerLink="whois"
routerLinkActive="active-link"
routerLink="rdap"
routerLinkActive
queryParamsHandling="merge"
>WHOIS Info</a
#rla2="routerLinkActive"
[active]="rla2.isActive"
aria-label="Access rdap settings"
>RDAP Info</a
>
<a
mat-tab-link
routerLink="security"
routerLinkActive="active-link"
routerLinkActive
queryParamsHandling="merge"
#rla3="routerLinkActive"
[active]="rla3.isActive"
aria-label="Access security settings"
>Security</a
>
</nav>

View File

@@ -13,14 +13,6 @@
// limitations under the License.
.console-settings {
.mdc-tab {
&.active-link {
border-bottom: 2px solid var(--primary);
.mdc-tab__text-label {
color: var(--primary);
}
}
}
nav {
margin-bottom: 40px;
}

View File

@@ -17,6 +17,12 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SettingsComponent } from './settings.component';
import { MaterialModule } from '../material.module';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ActivatedRoute } from '@angular/router';
import { AppModule, SelectedRegistrarModule } from '../app.module';
import { BackendService } from '../shared/services/backend.service';
import { provideHttpClient } from '@angular/common/http';
import { provideHttpClientTesting } from '@angular/common/http/testing';
import { AppRoutingModule } from '../app-routing.module';
describe('SettingsComponent', () => {
let component: SettingsComponent;
@@ -24,7 +30,19 @@ describe('SettingsComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [MaterialModule, BrowserAnimationsModule],
imports: [
SelectedRegistrarModule,
MaterialModule,
BrowserAnimationsModule,
AppRoutingModule,
AppModule,
],
providers: [
BackendService,
{ provide: ActivatedRoute, useValue: {} as ActivatedRoute },
provideHttpClient(),
provideHttpClientTesting(),
],
declarations: [SettingsComponent],
}).compileComponents();

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