diff --git a/docs/architecture/decisions/0023-use-geventconnpool.md b/docs/architecture/decisions/0023-use-geventconnpool.md
new file mode 100644
index 000000000..c24318b4f
--- /dev/null
+++ b/docs/architecture/decisions/0023-use-geventconnpool.md
@@ -0,0 +1,89 @@
+# 22. Use geventconnpool library for Connection Pooling
+
+Date: 2023-13-10
+
+## Status
+
+In Review
+
+## Context
+
+When sending and receiving data from the registry, we use the [EPPLib](https://github.com/cisagov/epplib) library to facilitate that process. To manage these connections within our application, we utilize a module named `epplibwrapper` which serves as a bridge between getgov and the EPPLib library. As part of this process, `epplibwrapper` will instantiate a client that handles sending/receiving data.
+
+At present, each time we need to send a command to the registry, the client establishes a new connection to handle this task. This becomes inefficient when dealing with multiple calls in parallel or in series, as we have to initiate a handshake for each of them. To mitigate this issue, a widely adopted solution is to use a [connection pool](https://en.wikipedia.org/wiki/Connection_pool). In general, a connection pool stores a cache of active connections so that rather than restarting the handshake process when it is unnecessary, we can utilize an existing connection to avoid this problem.
+
+In practice, the lack of a connection pool has resulted in performance issues when dealing with connections to and from the registry. Given the unique nature of our development stack, our options for prebuilt libraries are limited. Out of our available options, a library called [`geventconnpool`](https://github.com/rasky/geventconnpool) was identified that most closely matched our needs.
+
+## Considered Options
+
+**Option 1:** Use the existing connection pool library `geventconnpool`.
+
+➕ Pros
+
+- Saves development time and effort.
+- A tiny library that is easy to audit and understand.
+- Built to be flexible, so every built-in function can be overridden with minimal effort.
+- This library has been used for [EPP before](https://github.com/rasky/geventconnpool/issues/9).
+- Uses [`gevent`](http://www.gevent.org/) for coroutines, which is reliable and well maintained.
+- [`gevent`](http://www.gevent.org/) is used in our WSGI web server.
+- This library is the closest match to our needs that we have found.
+
+
+
+➖ Cons
+
+- Not a well maintained library, could require a fork if a dependency breaks.
+- Heavily reliant on `gevent`.
+
+
+
+**Option 2:** Write our own connection pool logic.
+
+➕ Pros
+
+- Full control over functionality, can be tailored to our specific needs.
+- Highly specific to our stack, could be fine tuned for performance.
+
+
+
+➖ Cons
+
+- Requires significant development time and effort, needs thorough testing.
+- Would require managing with and developing around concurrency.
+- Introduces the potential for many unseen bugs.
+
+
+
+**Option 3:** Modify an existing library which we will then tailor to our needs.
+
+➕ Pros
+
+- Savings in development time and effort, can be tailored to our specific needs.
+- Good middleground between the first two options.
+
+
+
+➖ Cons
+
+- Could introduce complexity, potential issues with maintaining the modified library.
+- May not be necessary if the given library is flexible enough.
+
+
+
+## Decision
+
+We have decided to go with option 1, which is to use the `geventconnpool` library. It closely matches our needs and offers several advantages. Of note, it significantly saves on development time and it is inherently flexible. This allows us to easily change functionality with minimal effort. In addition, the gevent library (which this uses) offers performance benefits due to it being a) written in [cython](https://cython.org/), b) very well maintained and purpose built for tasks such as these, and c) used in our WGSI server.
+
+In summary, this decision was driven by the library's flexibility, simplicity, and compatibility with our tech stack. We acknowledge the risk associated with its maintenance status, but believe that the benefit outweighs the risk.
+
+## Consequences
+
+While its small size makes it easy to work around, `geventconnpool` is not actively maintained. Its last update was in 2021, and as such there is a risk that its dependencies (gevent) will outpace this library and cause it to break. If such an event occurs, it would require that we fork the library and fix those issues. See option 3 pros/cons.
+
+## Mitigation Plan
+To manage this risk, we'll:
+
+1. Monitor the gevent library for updates.
+2. Design the connection pool logic abstractly such that we can easily swap the underlying logic out without needing (or minimizing the need) to rewrite code in `epplibwrapper`.
+3. Document a process for forking and maintaining the library if it becomes necessary, including testing procedures.
+4. Establish a contingency plan for reverting to a previous system state or switching to a different library if significant issues arise with `gevent` or `geventconnpool`.
\ No newline at end of file
diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js
index 57dc6d2e3..c21060382 100644
--- a/src/registrar/assets/js/get-gov.js
+++ b/src/registrar/assets/js/get-gov.js
@@ -236,28 +236,150 @@ function handleValidationClick(e) {
* Only does something on a single page, but it should be fast enough to run
* it everywhere.
*/
-(function prepareForms() {
- let serverForm = document.querySelectorAll(".server-form")
- let container = document.querySelector("#form-container")
- let addButton = document.querySelector("#add-form")
- let totalForms = document.querySelector("#id_form-TOTAL_FORMS")
+(function prepareNameserverForms() {
+ let serverForm = document.querySelectorAll(".server-form");
+ let container = document.querySelector("#form-container");
+ let addButton = document.querySelector("#add-nameserver-form");
+ let totalForms = document.querySelector("#id_form-TOTAL_FORMS");
- let formNum = serverForm.length-1
- addButton.addEventListener('click', addForm)
+ let formNum = serverForm.length-1;
+ if (addButton)
+ addButton.addEventListener('click', addForm);
function addForm(e){
- let newForm = serverForm[2].cloneNode(true)
- let formNumberRegex = RegExp(`form-(\\d){1}-`,'g')
- let formLabelRegex = RegExp(`Name server (\\d){1}`, 'g')
- let formExampleRegex = RegExp(`ns(\\d){1}`, 'g')
+ let newForm = serverForm[2].cloneNode(true);
+ let formNumberRegex = RegExp(`form-(\\d){1}-`,'g');
+ let formLabelRegex = RegExp(`Name server (\\d){1}`, 'g');
+ let formExampleRegex = RegExp(`ns(\\d){1}`, 'g');
- formNum++
- newForm.innerHTML = newForm.innerHTML.replace(formNumberRegex, `form-${formNum}-`)
- newForm.innerHTML = newForm.innerHTML.replace(formLabelRegex, `Name server ${formNum+1}`)
- newForm.innerHTML = newForm.innerHTML.replace(formExampleRegex, `ns${formNum+1}`)
- container.insertBefore(newForm, addButton)
- newForm.querySelector("input").value = ""
+ formNum++;
+ newForm.innerHTML = newForm.innerHTML.replace(formNumberRegex, `form-${formNum}-`);
+ newForm.innerHTML = newForm.innerHTML.replace(formLabelRegex, `Name server ${formNum+1}`);
+ newForm.innerHTML = newForm.innerHTML.replace(formExampleRegex, `ns${formNum+1}`);
+ container.insertBefore(newForm, addButton);
+ newForm.querySelector("input").value = "";
- totalForms.setAttribute('value', `${formNum+1}`)
+ totalForms.setAttribute('value', `${formNum+1}`);
}
})();
+
+function prepareDeleteButtons() {
+ let deleteButtons = document.querySelectorAll(".delete-record");
+ let totalForms = document.querySelector("#id_form-TOTAL_FORMS");
+
+ // Loop through each delete button and attach the click event listener
+ deleteButtons.forEach((deleteButton) => {
+ deleteButton.addEventListener('click', removeForm);
+ });
+
+ function removeForm(e){
+ let formToRemove = e.target.closest(".ds-record");
+ formToRemove.remove();
+ let forms = document.querySelectorAll(".ds-record");
+ totalForms.setAttribute('value', `${forms.length}`);
+
+ let formNumberRegex = RegExp(`form-(\\d){1}-`, 'g');
+ let formLabelRegex = RegExp(`DS Data record (\\d){1}`, 'g');
+
+ forms.forEach((form, index) => {
+ // Iterate over child nodes of the current element
+ Array.from(form.querySelectorAll('label, input, select')).forEach((node) => {
+ // Iterate through the attributes of the current node
+ Array.from(node.attributes).forEach((attr) => {
+ // Check if the attribute value matches the regex
+ if (formNumberRegex.test(attr.value)) {
+ // Replace the attribute value with the updated value
+ attr.value = attr.value.replace(formNumberRegex, `form-${index}-`);
+ }
+ });
+ });
+
+ Array.from(form.querySelectorAll('h2, legend')).forEach((node) => {
+ node.textContent = node.textContent.replace(formLabelRegex, `DS Data record ${index + 1}`);
+ });
+
+ });
+ }
+}
+
+/**
+ * An IIFE that attaches a click handler for our dynamic DNSSEC forms
+ *
+ */
+(function prepareDNSSECForms() {
+ let serverForm = document.querySelectorAll(".ds-record");
+ let container = document.querySelector("#form-container");
+ let addButton = document.querySelector("#add-ds-form");
+ let totalForms = document.querySelector("#id_form-TOTAL_FORMS");
+
+ // Attach click event listener on the delete buttons of the existing forms
+ prepareDeleteButtons();
+
+ // Attack click event listener on the add button
+ if (addButton)
+ addButton.addEventListener('click', addForm);
+
+ /*
+ * Add a formset to the end of the form.
+ * For each element in the added formset, name the elements with the prefix,
+ * form-{#}-{element_name} where # is the index of the formset and element_name
+ * is the element's name.
+ * Additionally, update the form element's metadata, including totalForms' value.
+ */
+ function addForm(e){
+ let forms = document.querySelectorAll(".ds-record");
+ let formNum = forms.length;
+ let newForm = serverForm[0].cloneNode(true);
+ let formNumberRegex = RegExp(`form-(\\d){1}-`,'g');
+ let formLabelRegex = RegExp(`DS Data record (\\d){1}`, 'g');
+
+ formNum++;
+ newForm.innerHTML = newForm.innerHTML.replace(formNumberRegex, `form-${formNum-1}-`);
+ newForm.innerHTML = newForm.innerHTML.replace(formLabelRegex, `DS Data record ${formNum}`);
+ container.insertBefore(newForm, addButton);
+
+ let inputs = newForm.querySelectorAll("input");
+ // Reset the values of each input to blank
+ inputs.forEach((input) => {
+ input.classList.remove("usa-input--error");
+ if (input.type === "text" || input.type === "number" || input.type === "password") {
+ input.value = ""; // Set the value to an empty string
+
+ } else if (input.type === "checkbox" || input.type === "radio") {
+ input.checked = false; // Uncheck checkboxes and radios
+ }
+ });
+
+ // Reset any existing validation classes
+ let selects = newForm.querySelectorAll("select");
+ selects.forEach((select) => {
+ select.classList.remove("usa-input--error");
+ select.selectedIndex = 0; // Set the value to an empty string
+ });
+
+ let labels = newForm.querySelectorAll("label");
+ labels.forEach((label) => {
+ label.classList.remove("usa-label--error");
+ });
+
+ let usaFormGroups = newForm.querySelectorAll(".usa-form-group");
+ usaFormGroups.forEach((usaFormGroup) => {
+ usaFormGroup.classList.remove("usa-form-group--error");
+ });
+
+ // Remove any existing error messages
+ let usaErrorMessages = newForm.querySelectorAll(".usa-error-message");
+ usaErrorMessages.forEach((usaErrorMessage) => {
+ let parentDiv = usaErrorMessage.closest('div');
+ if (parentDiv) {
+ parentDiv.remove(); // Remove the parent div if it exists
+ }
+ });
+
+ totalForms.setAttribute('value', `${formNum}`);
+
+ // Attach click event listener on the delete buttons of the new form
+ prepareDeleteButtons();
+ }
+
+})();
diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/sass/_theme/_admin.scss
index a2e32bd21..35d089cbd 100644
--- a/src/registrar/assets/sass/_theme/_admin.scss
+++ b/src/registrar/assets/sass/_theme/_admin.scss
@@ -179,4 +179,4 @@ h1, h2, h3 {
text-align: left;
background: var(--primary);
color: var(--header-link-color);
-}
\ No newline at end of file
+}
diff --git a/src/registrar/assets/sass/_theme/_alerts.scss b/src/registrar/assets/sass/_theme/_alerts.scss
new file mode 100644
index 000000000..dd51734ed
--- /dev/null
+++ b/src/registrar/assets/sass/_theme/_alerts.scss
@@ -0,0 +1,17 @@
+// Fixes some font size disparities with the Figma
+// for usa-alert alert elements
+.usa-alert {
+ .usa-alert__heading.larger-font-sizing {
+ font-size: units(3);
+ }
+}
+
+// The icon was off center for some reason
+// Fixes that issue
+@media (min-width: 64em){
+ .usa-alert--warning{
+ .usa-alert__body::before {
+ left: 1rem !important;
+ }
+ }
+}
diff --git a/src/registrar/assets/sass/_theme/_base.scss b/src/registrar/assets/sass/_theme/_base.scss
new file mode 100644
index 000000000..668a6ace6
--- /dev/null
+++ b/src/registrar/assets/sass/_theme/_base.scss
@@ -0,0 +1,127 @@
+@use "uswds-core" as *;
+
+/* Styles for making visible to screen reader / AT users only. */
+.sr-only {
+ @include sr-only;
+}
+
+* {
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+body {
+ display: flex;
+ flex-direction: column;
+ min-height: 100vh;
+}
+
+#wrapper {
+ flex-grow: 1;
+ padding-top: units(3);
+ padding-bottom: units(6) * 2 ; //Workaround because USWDS units jump from 10 to 15
+}
+
+#wrapper.dashboard {
+ background-color: color('primary-lightest');
+ padding-top: units(5);
+}
+
+.usa-logo {
+ @include at-media(desktop) {
+ margin-top: units(2);
+ }
+}
+
+.usa-logo__text {
+ @include typeset('sans', 'xl', 2);
+ color: color('primary-darker');
+}
+
+.usa-nav__primary {
+ margin-top: units(1);
+}
+
+.section--outlined {
+ background-color: color('white');
+ border: 1px solid color('base-lighter');
+ border-radius: 4px;
+ padding: 0 units(2) units(3);
+ margin-top: units(3);
+
+ h2 {
+ color: color('primary-dark');
+ margin-top: units(2);
+ margin-bottom: units(2);
+ }
+
+ p {
+ margin-bottom: 0;
+ }
+
+ @include at-media(mobile-lg) {
+ margin-top: units(5);
+
+ h2 {
+ margin-bottom: 0;
+ }
+ }
+}
+
+.break-word {
+ word-break: break-word;
+}
+
+.dotgov-status-box {
+ background-color: color('primary-lightest');
+ border-color: color('accent-cool-lighter');
+}
+
+.dotgov-status-box--action-need {
+ background-color: color('warning-lighter');
+ border-color: color('warning');
+}
+
+footer {
+ border-top: 1px solid color('primary-darker');
+}
+
+.usa-footer__secondary-section {
+ background-color: color('primary-lightest');
+}
+
+.usa-footer__secondary-section a {
+ color: color('primary');
+}
+
+.usa-identifier__logo {
+ height: units(7);
+}
+
+abbr[title] {
+ // workaround for underlining abbr element
+ border-bottom: none;
+ text-decoration: none;
+}
+
+@include at-media(tablet) {
+ .float-right-tablet {
+ float: right;
+ }
+ .float-left-tablet {
+ float: left;
+ }
+}
+
+@include at-media(desktop) {
+ .float-right-desktop {
+ float: right;
+ }
+ .float-left-desktop {
+ float: left;
+ }
+}
+
+.flex-end {
+ align-items: flex-end;
+}
diff --git a/src/registrar/assets/sass/_theme/_buttons.scss b/src/registrar/assets/sass/_theme/_buttons.scss
new file mode 100644
index 000000000..718bd5792
--- /dev/null
+++ b/src/registrar/assets/sass/_theme/_buttons.scss
@@ -0,0 +1,125 @@
+@use "uswds-core" as *;
+
+/* Make "placeholder" links visually obvious */
+a[href$="todo"]::after {
+ background-color: yellow;
+ color: color(blue-80v);
+ content: " [link TBD]";
+ font-style: italic;
+}
+
+a.breadcrumb__back {
+ display:flex;
+ align-items: center;
+ margin-bottom: units(2.5);
+ &:visited {
+ color: color('primary');
+ }
+
+ @include at-media('tablet') {
+ //align to top of sidebar
+ margin-top: units(-0.5);
+ }
+}
+
+a.usa-button {
+ text-decoration: none;
+ color: color('white');
+}
+
+a.usa-button:visited,
+a.usa-button:hover,
+a.usa-button:focus,
+a.usa-button:active {
+ color: color('white');
+}
+
+a.usa-button--outline,
+a.usa-button--outline:visited {
+ box-shadow: inset 0 0 0 2px color('primary');
+ color: color('primary');
+}
+
+a.usa-button--outline:hover,
+a.usa-button--outline:focus {
+ box-shadow: inset 0 0 0 2px color('primary-dark');
+ color: color('primary-dark');
+}
+
+a.usa-button--outline:active {
+ box-shadow: inset 0 0 0 2px color('primary-darker');
+ color: color('primary-darker');
+}
+
+a.withdraw {
+ background-color: color('error');
+}
+
+a.withdraw_outline,
+a.withdraw_outline:visited {
+ box-shadow: inset 0 0 0 2px color('error');
+ color: color('error');
+}
+
+a.withdraw_outline:hover,
+a.withdraw_outline:focus {
+ box-shadow: inset 0 0 0 2px color('error-dark');
+ color: color('error-dark');
+}
+
+a.withdraw_outline:active {
+ box-shadow: inset 0 0 0 2px color('error-darker');
+ color: color('error-darker');
+}
+
+a.withdraw:hover,
+a.withdraw:focus {
+ background-color: color('error-dark');
+}
+
+a.withdraw:active {
+ background-color: color('error-darker');
+}
+
+.usa-button--unstyled .usa-icon {
+ vertical-align: bottom;
+}
+
+a.usa-button--unstyled:visited {
+ color: color('primary');
+}
+
+.dotgov-button--green {
+ background-color: color('success-dark');
+
+ &:hover {
+ background-color: color('success-darker');
+ }
+
+ &:active {
+ background-color: color('green-80v');
+ }
+}
+
+// Cancel button used on the
+// DNSSEC main page
+// We want to center this button on mobile
+// and add some extra left margin on tablet+
+.usa-button--cancel {
+ text-align: center;
+ @include at-media('tablet') {
+ margin-left: units(2);
+ }
+}
+
+
+// WARNING: crazy hack ahead:
+// Cancel button(s) on the DNSSEC form pages
+// We want to position the cancel button on the
+// dnssec forms next to the submit button
+// This button's markup is in its own form
+.btn-cancel {
+ position: relative;
+ top: -39.2px;
+ left: 88px;
+}
diff --git a/src/registrar/assets/sass/_theme/_fieldsets.scss b/src/registrar/assets/sass/_theme/_fieldsets.scss
new file mode 100644
index 000000000..c60080cb9
--- /dev/null
+++ b/src/registrar/assets/sass/_theme/_fieldsets.scss
@@ -0,0 +1,10 @@
+@use "uswds-core" as *;
+
+fieldset {
+ border: solid 1px color('base-lighter');
+ padding: units(3);
+}
+
+fieldset:not(:first-child) {
+ margin-top: units(2);
+}
diff --git a/src/registrar/assets/sass/_theme/_forms.scss b/src/registrar/assets/sass/_theme/_forms.scss
new file mode 100644
index 000000000..ed118bb94
--- /dev/null
+++ b/src/registrar/assets/sass/_theme/_forms.scss
@@ -0,0 +1,25 @@
+@use "uswds-core" as *;
+
+.usa-form .usa-button {
+ margin-top: units(3);
+}
+
+.usa-form--extra-large {
+ max-width: none;
+}
+
+.usa-form--text-width {
+ max-width: measure(5);
+}
+
+.usa-textarea {
+ @include at-media('tablet') {
+ height: units('mobile');
+ }
+}
+
+.usa-form-group--unstyled-error {
+ margin-left: 0;
+ padding-left: 0;
+ border-left: none;
+}
diff --git a/src/registrar/assets/sass/_theme/_register-form.scss b/src/registrar/assets/sass/_theme/_register-form.scss
new file mode 100644
index 000000000..d0405a3c3
--- /dev/null
+++ b/src/registrar/assets/sass/_theme/_register-form.scss
@@ -0,0 +1,80 @@
+@use "uswds-core" as *;
+@use "typography" as *;
+
+.register-form-step > h1 {
+ //align to top of sidebar on first page of the form
+ margin-top: units(-1);
+}
+
+ //Tighter spacing when H2 is immediatly after H1
+.register-form-step .usa-fieldset:first-of-type h2:first-of-type,
+.register-form-step h1 + h2 {
+ margin-top: units(1);
+}
+
+.register-form-step h3 {
+ color: color('primary-dark');
+ letter-spacing: $letter-space--xs;
+ margin-top: units(3);
+ margin-bottom: 0;
+
+ + p {
+ margin-top: units(0.5);
+ }
+}
+
+.register-form-step h4 {
+ margin-bottom: 0;
+
+ + p {
+ margin-top: units(0.5);
+ }
+}
+
+.register-form-step a {
+ color: color('primary');
+
+ &:visited {
+ color: color('violet-70v'); //USWDS default
+ }
+}
+.register-form-step .usa-form-group:first-of-type,
+.register-form-step .usa-label:first-of-type {
+ margin-top: units(1);
+}
+
+.ao_example p {
+ margin-top: units(1);
+}
+
+.domain_example {
+ p {
+ margin-bottom: 0;
+ }
+
+ .usa-list {
+ margin-top: units(0.5);
+ }
+}
+
+.review__step {
+ margin-top: units(3);
+}
+
+ .summary-item hr,
+.review__step hr {
+ border: none; //reset
+ border-top: 1px solid color('primary-dark');
+ margin-top: 0;
+ margin-bottom: units(0.5);
+}
+
+.review__step__title a:visited {
+ color: color('primary');
+}
+
+.review__step__name {
+ color: color('primary-dark');
+ font-weight: font-weight('semibold');
+ margin-bottom: units(0.5);
+}
diff --git a/src/registrar/assets/sass/_theme/_sidenav.scss b/src/registrar/assets/sass/_theme/_sidenav.scss
new file mode 100644
index 000000000..caafa7dd4
--- /dev/null
+++ b/src/registrar/assets/sass/_theme/_sidenav.scss
@@ -0,0 +1,30 @@
+@use "uswds-core" as *;
+
+.usa-sidenav {
+ .usa-sidenav__item {
+ span {
+ a.link_usa-checked {
+ padding: 0;
+ }
+ }
+ }
+}
+
+.sidenav__step--locked {
+ color: color('base-darker');
+ span {
+ display: flex;
+ align-items: flex-start;
+ padding: units(1);
+
+ .usa-icon {
+ flex-shrink: 0;
+ //align lock body to x-height
+ margin: units('2px') units(1) 0 0;
+ }
+ }
+}
+
+.stepnav {
+ margin-top: units(2);
+}
diff --git a/src/registrar/assets/sass/_theme/_tables.scss b/src/registrar/assets/sass/_theme/_tables.scss
new file mode 100644
index 000000000..6dcc6f3bc
--- /dev/null
+++ b/src/registrar/assets/sass/_theme/_tables.scss
@@ -0,0 +1,93 @@
+@use "uswds-core" as *;
+
+.dotgov-table--stacked {
+ td, th {
+ padding: units(1) units(2) units(2px) 0;
+ border: none;
+ }
+
+ tr:first-child th:first-child {
+ border-top: none;
+ }
+
+ tr {
+ border-bottom: none;
+ border-top: 2px solid color('base-light');
+ margin-top: units(2);
+
+ &:first-child {
+ margin-top: 0;
+ }
+ }
+
+ td[data-label]:before,
+ th[data-label]:before {
+ color: color('primary-darker');
+ padding-bottom: units(2px);
+ }
+}
+
+.dotgov-table {
+ width: 100%;
+
+ a {
+ display: flex;
+ align-items: flex-start;
+ color: color('primary');
+
+ &:visited {
+ color: color('primary');
+ }
+
+ .usa-icon {
+ // align icon with x height
+ margin-top: units(0.5);
+ margin-right: units(0.5);
+ }
+ }
+
+ th[data-sortable]:not([aria-sort]) .usa-table__header__button {
+ right: auto;
+ }
+
+ tbody th {
+ word-break: break-word;
+ }
+
+ @include at-media(mobile-lg) {
+
+ margin-top: units(1);
+
+ tr {
+ border: none;
+ }
+
+ td, th {
+ border-bottom: 1px solid color('base-light');
+ }
+
+ thead th {
+ color: color('primary-darker');
+ border-bottom: 2px solid color('base-light');
+ }
+
+ tbody tr:last-of-type {
+ td, th {
+ border-bottom: 0;
+ }
+ }
+
+ td, th,
+ .usa-tabel th{
+ padding: units(2) units(2) units(2) 0;
+ }
+
+ th:first-of-type {
+ padding-left: 0;
+ }
+
+ thead tr:first-child th:first-child {
+ border-top: none;
+ }
+ }
+}
diff --git a/src/registrar/assets/sass/_theme/_typography.scss b/src/registrar/assets/sass/_theme/_typography.scss
new file mode 100644
index 000000000..4fc2bb819
--- /dev/null
+++ b/src/registrar/assets/sass/_theme/_typography.scss
@@ -0,0 +1,24 @@
+@use "uswds-core" as *;
+
+// Finer grained letterspacing adjustments
+$letter-space--xs: .0125em;
+
+p,
+address,
+.usa-list li {
+ @include typeset('sans', 'sm', 5);
+ max-width: measure(5);
+}
+
+h1 {
+ @include typeset('sans', '2xl', 2);
+ margin: 0 0 units(2);
+ color: color('primary-darker');
+}
+
+h2 {
+ font-weight: font-weight('semibold');
+ line-height: line-height('heading', 3);
+ margin: units(4) 0 units(1);
+ color: color('primary-darker');
+}
diff --git a/src/registrar/assets/sass/_theme/_uswds-theme-custom-styles.scss b/src/registrar/assets/sass/_theme/_uswds-theme-custom-styles.scss
deleted file mode 100644
index e69b36bb8..000000000
--- a/src/registrar/assets/sass/_theme/_uswds-theme-custom-styles.scss
+++ /dev/null
@@ -1,457 +0,0 @@
-/*
-* * * * * ==============================
-* * * * * ==============================
-* * * * * ==============================
-* * * * * ==============================
-========================================
-========================================
-========================================
-----------------------------------------
-USWDS THEME CUSTOM STYLES
-----------------------------------------
-!! Copy this file to your project's
- sass root. Don't edit the version
- in node_modules.
-----------------------------------------
-Custom project SASS goes here.
-
-i.e.
-@include u-padding-right('05');
-----------------------------------------
-*/
-
-// Finer grained letterspacing adjustments
-$letter-space--xs: .0125em;
-
-@use "uswds-core" as *;
-
-/* Styles for making visible to screen reader / AT users only. */
-.sr-only {
- @include sr-only;
- }
-
- * {
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
-}
-
-body {
- display: flex;
- flex-direction: column;
- min-height: 100vh;
-}
-
-#wrapper {
- flex-grow: 1;
-}
-
-.usa-logo {
- @include at-media(desktop) {
- margin-top: units(2);
- }
-}
-
-.usa-logo__text {
- @include typeset('sans', 'xl', 2);
- color: color('primary-darker');
-}
-
-.usa-nav__primary {
- margin-top: units(1);
-}
-
-p,
-address,
-.usa-list li {
- @include typeset('sans', 'sm', 5);
- max-width: measure(5);
-}
-
-h1 {
- @include typeset('sans', '2xl', 2);
- margin: 0 0 units(2);
- color: color('primary-darker');
-}
-
-h2 {
- font-weight: font-weight('semibold');
- line-height: line-height('heading', 3);
- margin: units(4) 0 units(1);
- color: color('primary-darker');
-}
-
-.register-form-step > h1 {
- //align to top of sidebar on first page of the form
- margin-top: units(-1);
-}
-
- //Tighter spacing when H2 is immediatly after H1
-.register-form-step .usa-fieldset:first-of-type h2:first-of-type,
-.register-form-step h1 + h2 {
- margin-top: units(1);
-}
-
-.register-form-step h3 {
- color: color('primary-dark');
- letter-spacing: $letter-space--xs;
- margin-top: units(3);
- margin-bottom: 0;
-
- + p {
- margin-top: units(0.5);
- }
-}
-
-.register-form-step h4 {
- margin-bottom: 0;
-
- + p {
- margin-top: units(0.5);
- }
-}
-
-
-.register-form-step a {
- color: color('primary');
-
- &:visited {
- color: color('violet-70v'); //USWDS default
- }
-}
-.register-form-step .usa-form-group:first-of-type,
-.register-form-step .usa-label:first-of-type {
- margin-top: units(1);
-}
-
-/* Make "placeholder" links visually obvious */
-a[href$="todo"]::after {
- background-color: yellow;
- color: color(blue-80v);
- content: " [link TBD]";
- font-style: italic;
-}
-
-a.breadcrumb__back {
- display:flex;
- align-items: center;
- margin-bottom: units(2.5);
- &:visited {
- color: color('primary');
- }
-
- @include at-media('tablet') {
- //align to top of sidebar
- margin-top: units(-0.5);
- }
-}
-
-a.withdraw {
- background-color: color('error');
-}
-
-a.withdraw_outline,
-a.withdraw_outline:visited {
- box-shadow: inset 0 0 0 2px color('error');
- color: color('error');
-}
-
-a.withdraw_outline:hover,
-a.withdraw_outline:focus {
- box-shadow: inset 0 0 0 2px color('error-dark');
- color: color('error-dark');
-}
-
-a.withdraw_outline:active {
- box-shadow: inset 0 0 0 2px color('error-darker');
- color: color('error-darker');
-}
-a.withdraw:hover,
-a.withdraw:focus {
- background-color: color('error-dark');
-}
-
-a.withdraw:active {
- background-color: color('error-darker');
-}
-
-.usa-sidenav {
- .usa-sidenav__item {
- span {
- a.link_usa-checked {
- padding: 0;
- }
- }
- }
-}
-
-.sidenav__step--locked {
- color: color('base-darker');
- span {
- display: flex;
- align-items: flex-start;
- padding: units(1);
-
- .usa-icon {
- flex-shrink: 0;
- //align lock body to x-height
- margin: units('2px') units(1) 0 0;
- }
- }
-}
-
-
-.stepnav {
- margin-top: units(2);
-}
-
-.ao_example p {
- margin-top: units(1);
-}
-
-.domain_example {
- p {
- margin-bottom: 0;
- }
-
- .usa-list {
- margin-top: units(0.5);
- }
-}
-
-.review__step {
- margin-top: units(3);
-}
-
-.summary-item hr,
-.review__step hr {
- border: none; //reset
- border-top: 1px solid color('primary-dark');
- margin-top: 0;
- margin-bottom: units(0.5);
-}
-
-.review__step__title a:visited {
- color: color('primary');
-}
-
-.review__step__name {
- color: color('primary-dark');
- font-weight: font-weight('semibold');
- margin-bottom: units(0.5);
-}
-
-.usa-form .usa-button {
- margin-top: units(3);
-}
-
-.usa-button--unstyled .usa-icon {
- vertical-align: bottom;
-}
-
-a.usa-button--unstyled:visited {
- color: color('primary');
-}
-
-.dotgov-button--green {
- background-color: color('success-dark');
-
- &:hover {
- background-color: color('success-darker');
- }
-
- &:active {
- background-color: color('green-80v');
- }
-}
-
-/** ---- DASHBOARD ---- */
-
-#wrapper.dashboard {
- background-color: color('primary-lightest');
- padding-top: units(5);
-}
-
-.section--outlined {
- background-color: color('white');
- border: 1px solid color('base-lighter');
- border-radius: 4px;
- padding: 0 units(2) units(3);
- margin-top: units(3);
-
- h2 {
- color: color('primary-dark');
- margin-top: units(2);
- margin-bottom: units(2);
- }
-
- p {
- margin-bottom: 0;
- }
-
- @include at-media(mobile-lg) {
- margin-top: units(5);
-
- h2 {
- margin-bottom: 0;
- }
- }
-}
-
-.dotgov-table--stacked {
- td, th {
- padding: units(1) units(2) units(2px) 0;
- border: none;
- }
-
- tr:first-child th:first-child {
- border-top: none;
- }
-
- tr {
- border-bottom: none;
- border-top: 2px solid color('base-light');
- margin-top: units(2);
-
- &:first-child {
- margin-top: 0;
- }
- }
-
- td[data-label]:before,
- th[data-label]:before {
- color: color('primary-darker');
- padding-bottom: units(2px);
- }
-}
-
-.dotgov-table {
- width: 100%;
-
- a {
- display: flex;
- align-items: flex-start;
- color: color('primary');
-
- &:visited {
- color: color('primary');
- }
-
- .usa-icon {
- // align icon with x height
- margin-top: units(0.5);
- margin-right: units(0.5);
- }
- }
-
- th[data-sortable]:not([aria-sort]) .usa-table__header__button {
- right: auto;
- }
-
- tbody th {
- word-break: break-word;
- }
-
-
- @include at-media(mobile-lg) {
-
- margin-top: units(1);
-
- tr {
- border: none;
- }
-
- td, th {
- border-bottom: 1px solid color('base-light');
- }
-
- thead th {
- color: color('primary-darker');
- border-bottom: 2px solid color('base-light');
- }
-
- tbody tr:last-of-type {
- td, th {
- border-bottom: 0;
- }
- }
-
- td, th,
- .usa-tabel th{
- padding: units(2) units(2) units(2) 0;
- }
-
- th:first-of-type {
- padding-left: 0;
- }
-
- thead tr:first-child th:first-child {
- border-top: none;
- }
- }
-}
-
-.break-word {
- word-break: break-word;
-}
-
-.dotgov-status-box {
- background-color: color('primary-lightest');
- border-color: color('accent-cool-lighter');
-}
-
-.dotgov-status-box--action-need {
- background-color: color('warning-lighter');
- border-color: color('warning');
-}
-
-#wrapper {
- padding-top: units(3);
- padding-bottom: units(6) * 2 ; //Workaround because USWDS units jump from 10 to 15
-}
-
-
-footer {
- border-top: 1px solid color('primary-darker');
-}
-
-.usa-footer__secondary-section {
- background-color: color('primary-lightest');
-}
-
-.usa-footer__secondary-section a {
- color: color('primary');
-}
-
-.usa-identifier__logo {
- height: units(7);
-}
-
-abbr[title] {
- // workaround for underlining abbr element
- border-bottom: none;
- text-decoration: none;
-}
-
-.usa-textarea {
- @include at-media('tablet') {
- height: units('mobile');
- }
-}
-
-// Fixes some font size disparities with the Figma
-// for usa-alert alert elements
-.usa-alert {
- .usa-alert__heading.larger-font-sizing {
- font-size: units(3);
- }
-}
-
-// The icon was off center for some reason
-// Fixes that issue
-@media (min-width: 64em){
- .usa-alert--warning{
- .usa-alert__body::before {
- left: 1rem !important;
- }
- }
-}
diff --git a/src/registrar/assets/sass/_theme/styles.scss b/src/registrar/assets/sass/_theme/styles.scss
index 27d844760..8a2e1d2d3 100644
--- a/src/registrar/assets/sass/_theme/styles.scss
+++ b/src/registrar/assets/sass/_theme/styles.scss
@@ -8,7 +8,15 @@
/*--------------------------------------------------
--- Custom Styles ---------------------------------*/
-@forward "uswds-theme-custom-styles";
+@forward "base";
+@forward "typography";
+@forward "buttons";
+@forward "forms";
+@forward "fieldsets";
+@forward "alerts";
+@forward "tables";
+@forward "sidenav";
+@forward "register-form";
/*--------------------------------------------------
--- Admin ---------------------------------*/
diff --git a/src/registrar/config/urls.py b/src/registrar/config/urls.py
index 9c3624c2c..bd2215620 100644
--- a/src/registrar/config/urls.py
+++ b/src/registrar/config/urls.py
@@ -81,9 +81,29 @@ urlpatterns = [
path("domain/", views.DomainView.as_view(), name="domain"),
path("domain//users", views.DomainUsersView.as_view(), name="domain-users"),
path(
- "domain//nameservers",
+ "domain//dns",
+ views.DomainDNSView.as_view(),
+ name="domain-dns",
+ ),
+ path(
+ "domain//dns/nameservers",
views.DomainNameserversView.as_view(),
- name="domain-nameservers",
+ name="domain-dns-nameservers",
+ ),
+ path(
+ "domain//dns/dnssec",
+ views.DomainDNSSECView.as_view(),
+ name="domain-dns-dnssec",
+ ),
+ path(
+ "domain//dns/dnssec/dsdata",
+ views.DomainDsDataView.as_view(),
+ name="domain-dns-dnssec-dsdata",
+ ),
+ path(
+ "domain//dns/dnssec/keydata",
+ views.DomainKeyDataView.as_view(),
+ name="domain-dns-dnssec-keydata",
),
path(
"domain//your-contact-information",
diff --git a/src/registrar/forms/__init__.py b/src/registrar/forms/__init__.py
index 13f75563f..7d2baf646 100644
--- a/src/registrar/forms/__init__.py
+++ b/src/registrar/forms/__init__.py
@@ -5,4 +5,9 @@ from .domain import (
DomainSecurityEmailForm,
DomainOrgNameAddressForm,
ContactForm,
+ DomainDnssecForm,
+ DomainDsdataFormset,
+ DomainDsdataForm,
+ DomainKeydataFormset,
+ DomainKeydataForm,
)
diff --git a/src/registrar/forms/common.py b/src/registrar/forms/common.py
new file mode 100644
index 000000000..159113488
--- /dev/null
+++ b/src/registrar/forms/common.py
@@ -0,0 +1,38 @@
+# common.py
+#
+# ALGORITHM_CHOICES are options for alg attribute in DS Data and Key Data
+# reference:
+# https://www.iana.org/assignments/dns-sec-alg-numbers/dns-sec-alg-numbers.xhtml
+ALGORITHM_CHOICES = [
+ (1, "(1) ERSA/MD5 [RSAMD5]"),
+ (2, "(2) Diffie-Hellman [DH]"),
+ (3, "(3) DSA/SHA-1 [DSA]"),
+ (5, "(5) RSA/SHA-1 [RSASHA1]"),
+ (6, "(6) DSA-NSEC3-SHA1"),
+ (7, "(7) RSASHA1-NSEC3-SHA1"),
+ (8, "(8) RSA/SHA-256 [RSASHA256]"),
+ (10, "(10) RSA/SHA-512 [RSASHA512]"),
+ (12, "(12) GOST R 34.10-2001 [ECC-GOST]"),
+ (13, "(13) ECDSA Curve P-256 with SHA-256 [ECDSAP256SHA256]"),
+ (14, "(14) ECDSA Curve P-384 with SHA-384 [ECDSAP384SHA384]"),
+ (15, "(15) Ed25519"),
+ (16, "(16) Ed448"),
+]
+# DIGEST_TYPE_CHOICES are options for digestType attribute in DS Data
+# reference: https://datatracker.ietf.org/doc/html/rfc4034#appendix-A.2
+DIGEST_TYPE_CHOICES = [
+ (0, "(0) Reserved"),
+ (1, "(1) SHA-256"),
+]
+# PROTOCOL_CHOICES are options for protocol attribute in Key Data
+# reference: https://datatracker.ietf.org/doc/html/rfc4034#section-2.1.2
+PROTOCOL_CHOICES = [
+ (3, "(3) DNSSEC"),
+]
+# FLAG_CHOICES are options for flags attribute in Key Data
+# reference: https://datatracker.ietf.org/doc/html/rfc4034#section-2.1.1
+FLAG_CHOICES = [
+ (0, "(0)"),
+ (256, "(256) ZSK"),
+ (257, "(257) KSK"),
+]
diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py
index 79fe46add..8abc7e14a 100644
--- a/src/registrar/forms/domain.py
+++ b/src/registrar/forms/domain.py
@@ -1,23 +1,27 @@
"""Forms for domain management."""
from django import forms
-from django.core.validators import RegexValidator
+from django.core.validators import MinValueValidator, MaxValueValidator, RegexValidator
from django.forms import formset_factory
from phonenumber_field.widgets import RegionalPhoneNumberWidget
from ..models import Contact, DomainInformation
+from .common import (
+ ALGORITHM_CHOICES,
+ DIGEST_TYPE_CHOICES,
+ FLAG_CHOICES,
+ PROTOCOL_CHOICES,
+)
class DomainAddUserForm(forms.Form):
-
"""Form for adding a user to a domain."""
email = forms.EmailField(label="Email")
class DomainNameserverForm(forms.Form):
-
"""Form for changing nameservers."""
server = forms.CharField(label="Name server", strip=True)
@@ -31,7 +35,6 @@ NameserverFormset = formset_factory(
class ContactForm(forms.ModelForm):
-
"""Form for updating contacts."""
class Meta:
@@ -62,14 +65,12 @@ class ContactForm(forms.ModelForm):
class DomainSecurityEmailForm(forms.Form):
-
"""Form for adding or editing a security email to a domain."""
security_email = forms.EmailField(label="Security email", required=False)
class DomainOrgNameAddressForm(forms.ModelForm):
-
"""Form for updating the organization name and mailing address."""
zipcode = forms.CharField(
@@ -140,3 +141,91 @@ class DomainOrgNameAddressForm(forms.ModelForm):
self.fields[field_name].required = True
self.fields["state_territory"].widget.attrs.pop("maxlength", None)
self.fields["zipcode"].widget.attrs.pop("maxlength", None)
+
+
+class DomainDnssecForm(forms.Form):
+ """Form for enabling and disabling dnssec"""
+
+
+class DomainDsdataForm(forms.Form):
+ """Form for adding or editing DNSSEC DS Data to a domain."""
+
+ key_tag = forms.IntegerField(
+ required=True,
+ label="Key tag",
+ validators=[
+ MinValueValidator(0, message="Value must be between 0 and 65535"),
+ MaxValueValidator(65535, message="Value must be between 0 and 65535"),
+ ],
+ error_messages={"required": ("Key tag is required.")},
+ )
+
+ algorithm = forms.TypedChoiceField(
+ required=True,
+ label="Algorithm",
+ coerce=int, # need to coerce into int so dsData objects can be compared
+ choices=[(None, "--Select--")] + ALGORITHM_CHOICES, # type: ignore
+ error_messages={"required": ("Algorithm is required.")},
+ )
+
+ digest_type = forms.TypedChoiceField(
+ required=True,
+ label="Digest type",
+ coerce=int, # need to coerce into int so dsData objects can be compared
+ choices=[(None, "--Select--")] + DIGEST_TYPE_CHOICES, # type: ignore
+ error_messages={"required": ("Digest Type is required.")},
+ )
+
+ digest = forms.CharField(
+ required=True,
+ label="Digest",
+ error_messages={"required": ("Digest is required.")},
+ )
+
+
+DomainDsdataFormset = formset_factory(
+ DomainDsdataForm,
+ extra=0,
+ can_delete=True,
+)
+
+
+class DomainKeydataForm(forms.Form):
+ """Form for adding or editing DNSSEC Key Data to a domain."""
+
+ flag = forms.TypedChoiceField(
+ required=True,
+ label="Flag",
+ coerce=int,
+ choices=FLAG_CHOICES,
+ error_messages={"required": ("Flag is required.")},
+ )
+
+ protocol = forms.TypedChoiceField(
+ required=True,
+ label="Protocol",
+ coerce=int,
+ choices=PROTOCOL_CHOICES,
+ error_messages={"required": ("Protocol is required.")},
+ )
+
+ algorithm = forms.TypedChoiceField(
+ required=True,
+ label="Algorithm",
+ coerce=int,
+ choices=[(None, "--Select--")] + ALGORITHM_CHOICES, # type: ignore
+ error_messages={"required": ("Algorithm is required.")},
+ )
+
+ pub_key = forms.CharField(
+ required=True,
+ label="Pub key",
+ error_messages={"required": ("Pub key is required.")},
+ )
+
+
+DomainKeydataFormset = formset_factory(
+ DomainKeydataForm,
+ extra=0,
+ can_delete=True,
+)
diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py
index 07d92f757..f66221338 100644
--- a/src/registrar/models/domain.py
+++ b/src/registrar/models/domain.py
@@ -4,6 +4,7 @@ import ipaddress
import re
from datetime import date
from string import digits
+from typing import Optional
from django_fsm import FSMField, transition, TransitionNotAllowed # type: ignore
from django.db import models
@@ -457,24 +458,140 @@ class Domain(TimeStampedModel, DomainHelper):
return [deleteObj], len(deleteStrList)
@Cache
- def dnssecdata(self) -> extensions.DNSSECExtension:
- return self._get_property("dnssecdata")
+ def dnssecdata(self) -> Optional[extensions.DNSSECExtension]:
+ """
+ Get a complete list of dnssecdata extensions for this domain.
+
+ dnssecdata are provided as a list of DNSSECExtension objects.
+
+ A DNSSECExtension object includes:
+ maxSigLife: Optional[int]
+ dsData: Optional[Sequence[DSData]]
+ keyData: Optional[Sequence[DNSSECKeyData]]
+
+ """
+ try:
+ return self._get_property("dnssecdata")
+ except Exception as err:
+ # Don't throw error as this is normal for a new domain
+ logger.info("Domain does not have dnssec data defined %s" % err)
+ return None
+
+ def getDnssecdataChanges(
+ self, _dnssecdata: Optional[extensions.DNSSECExtension]
+ ) -> tuple[dict, dict]:
+ """
+ calls self.dnssecdata, it should pull from cache but may result
+ in an epp call
+ returns tuple of 2 values as follows:
+ addExtension: dict
+ remExtension: dict
+
+ addExtension includes all dsData or keyData to be added
+ remExtension includes all dsData or keyData to be removed
+
+ method operates on dsData OR keyData, never a mix of the two;
+ operates based on which is present in _dnssecdata;
+ if neither is present, addExtension will be empty dict, and
+ remExtension will be all existing dnssecdata to be deleted
+ """
+
+ oldDnssecdata = self.dnssecdata
+ addDnssecdata: dict = {}
+ remDnssecdata: dict = {}
+
+ if _dnssecdata and _dnssecdata.dsData is not None:
+ # initialize addDnssecdata and remDnssecdata for dsData
+ addDnssecdata["dsData"] = _dnssecdata.dsData
+
+ if oldDnssecdata and len(oldDnssecdata.dsData) > 0:
+ # if existing dsData not in new dsData, mark for removal
+ dsDataForRemoval = [
+ dsData
+ for dsData in oldDnssecdata.dsData
+ if dsData not in _dnssecdata.dsData
+ ]
+ if len(dsDataForRemoval) > 0:
+ remDnssecdata["dsData"] = dsDataForRemoval
+
+ # if new dsData not in existing dsData, mark for add
+ dsDataForAdd = [
+ dsData
+ for dsData in _dnssecdata.dsData
+ if dsData not in oldDnssecdata.dsData
+ ]
+ if len(dsDataForAdd) > 0:
+ addDnssecdata["dsData"] = dsDataForAdd
+ else:
+ addDnssecdata["dsData"] = None
+
+ elif _dnssecdata and _dnssecdata.keyData is not None:
+ # initialize addDnssecdata and remDnssecdata for keyData
+ addDnssecdata["keyData"] = _dnssecdata.keyData
+
+ if oldDnssecdata and len(oldDnssecdata.keyData) > 0:
+ # if existing keyData not in new keyData, mark for removal
+ keyDataForRemoval = [
+ keyData
+ for keyData in oldDnssecdata.keyData
+ if keyData not in _dnssecdata.keyData
+ ]
+ if len(keyDataForRemoval) > 0:
+ remDnssecdata["keyData"] = keyDataForRemoval
+
+ # if new keyData not in existing keyData, mark for add
+ keyDataForAdd = [
+ keyData
+ for keyData in _dnssecdata.keyData
+ if keyData not in oldDnssecdata.keyData
+ ]
+ if len(keyDataForAdd) > 0:
+ addDnssecdata["keyData"] = keyDataForAdd
+ else:
+ # there are no new dsData or keyData, remove all
+ remDnssecdata["dsData"] = getattr(oldDnssecdata, "dsData", None)
+ remDnssecdata["keyData"] = getattr(oldDnssecdata, "keyData", None)
+
+ return addDnssecdata, remDnssecdata
@dnssecdata.setter # type: ignore
- def dnssecdata(self, _dnssecdata: extensions.DNSSECExtension):
- updateParams = {
- "maxSigLife": _dnssecdata.get("maxSigLife", None),
- "dsData": _dnssecdata.get("dsData", None),
- "keyData": _dnssecdata.get("keyData", None),
- "remAllDsKeyData": True,
+ def dnssecdata(self, _dnssecdata: Optional[extensions.DNSSECExtension]):
+ _addDnssecdata, _remDnssecdata = self.getDnssecdataChanges(_dnssecdata)
+ addParams = {
+ "maxSigLife": _addDnssecdata.get("maxSigLife", None),
+ "dsData": _addDnssecdata.get("dsData", None),
+ "keyData": _addDnssecdata.get("keyData", None),
}
- request = commands.UpdateDomain(name=self.name)
- extension = commands.UpdateDomainDNSSECExtension(**updateParams)
- request.add_extension(extension)
+ remParams = {
+ "maxSigLife": _remDnssecdata.get("maxSigLife", None),
+ "remDsData": _remDnssecdata.get("dsData", None),
+ "remKeyData": _remDnssecdata.get("keyData", None),
+ }
+ addRequest = commands.UpdateDomain(name=self.name)
+ addExtension = commands.UpdateDomainDNSSECExtension(**addParams)
+ addRequest.add_extension(addExtension)
+ remRequest = commands.UpdateDomain(name=self.name)
+ remExtension = commands.UpdateDomainDNSSECExtension(**remParams)
+ remRequest.add_extension(remExtension)
try:
- registry.send(request, cleaned=True)
+ if (
+ "dsData" in _addDnssecdata
+ and _addDnssecdata["dsData"] is not None
+ or "keyData" in _addDnssecdata
+ and _addDnssecdata["keyData"] is not None
+ ):
+ registry.send(addRequest, cleaned=True)
+ if (
+ "dsData" in _remDnssecdata
+ and _remDnssecdata["dsData"] is not None
+ or "keyData" in _remDnssecdata
+ and _remDnssecdata["keyData"] is not None
+ ):
+ registry.send(remRequest, cleaned=True)
except RegistryError as e:
- logger.error("Error adding DNSSEC, code was %s error was %s" % (e.code, e))
+ logger.error(
+ "Error updating DNSSEC, code was %s error was %s" % (e.code, e)
+ )
raise e
@nameservers.setter # type: ignore
diff --git a/src/registrar/templates/domain_detail.html b/src/registrar/templates/domain_detail.html
index bcf775fe5..e0d672093 100644
--- a/src/registrar/templates/domain_detail.html
+++ b/src/registrar/templates/domain_detail.html
@@ -27,7 +27,7 @@
- {% url 'domain-nameservers' pk=domain.id as url %}
+ {% url 'domain-dns-nameservers' pk=domain.id as url %}
{% if domain.nameservers|length > 0 %}
{% include "includes/summary_item.html" with title='DNS name servers' value=domain.nameservers list='true' edit_link=url %}
{% else %}
diff --git a/src/registrar/templates/domain_dns.html b/src/registrar/templates/domain_dns.html
new file mode 100644
index 000000000..b16c1cb8b
--- /dev/null
+++ b/src/registrar/templates/domain_dns.html
@@ -0,0 +1,20 @@
+{% extends "domain_base.html" %}
+{% load static field_helpers url_helpers %}
+
+{% block title %}DNS | {{ domain.name }} | {% endblock %}
+
+{% block domain_content %}
+
+
DNS
+
+
The Domain Name System (DNS) is the internet service that translates your domain name into an IP address. Before your .gov domain can be used, you'll need to connect it to your DNS hosting service and provide us with your name server information.
+
+
You can enter your name servers, as well as other DNS-related information, in the following sections:
+
+ {% url 'domain-dns-nameservers' pk=domain.id as url %}
+
DNSSEC, or DNS Security Extensions, is additional security layer to protect your domain. Enabling DNSSEC ensures that when someone visits your domain, they can be certain that it's connecting to the correct server, preventing potential hijacking or tampering with your domain's records.
+
+
+
+
+ {% include 'includes/modal.html' with modal_heading="Are you sure you want to continue?" modal_description="Your DNSSEC records will be deleted from the registry." modal_button=modal_button|safe %}
+
+
+{% endblock %} {# domain_content #}
diff --git a/src/registrar/templates/domain_dsdata.html b/src/registrar/templates/domain_dsdata.html
new file mode 100644
index 000000000..ca4dce783
--- /dev/null
+++ b/src/registrar/templates/domain_dsdata.html
@@ -0,0 +1,123 @@
+{% extends "domain_base.html" %}
+{% load static field_helpers url_helpers %}
+
+{% block title %}DS Data | {{ domain.name }} | {% endblock %}
+
+{% block domain_content %}
+ {% for form in formset %}
+ {% include "includes/form_errors.html" with form=form %}
+ {% endfor %}
+
+ {% if domain.dnssecdata is None and not dnssec_ds_confirmed %}
+
+
+ You have no DS Data added. Enable DNSSEC by adding DS Data or return to the DNSSEC page and click 'enable.'
+
+
+ {% endif %}
+
+
DS Data
+
+ {% if domain.dnssecdata is not None and domain.dnssecdata.keyData is not None %}
+
+
+
Warning, you cannot add DS Data
+
+ You cannot add DS Data because you have already added Key Data. Delete your Key Data records in order to add DS Data.
+
+
+
+ {% elif not dnssec_ds_confirmed %}
+
In order to enable DNSSEC, you must first configure it with your DNS hosting service.
+
Enter the values given by your DNS provider for DS Data.
+
Required fields are marked with an asterisk (*).
+
+ {% else %}
+
+
Enter the values given by your DNS provider for DS Data.
+ {% include "includes/required_fields.html" %}
+
+
+
+
+ {% endif %}
+{% endblock %} {# domain_content #}
diff --git a/src/registrar/templates/domain_keydata.html b/src/registrar/templates/domain_keydata.html
new file mode 100644
index 000000000..167d86370
--- /dev/null
+++ b/src/registrar/templates/domain_keydata.html
@@ -0,0 +1,110 @@
+{% extends "domain_base.html" %}
+{% load static field_helpers url_helpers %}
+
+{% block title %}Key Data | {{ domain.name }} | {% endblock %}
+
+{% block domain_content %}
+ {% for form in formset %}
+ {% include "includes/form_errors.html" with form=form %}
+ {% endfor %}
+
+
Key Data
+
+ {% if domain.dnssecdata is not None and domain.dnssecdata.dsData is not None %}
+
+
+
Warning, you cannot add Key Data
+
+ You cannot add Key Data because you have already added DS Data. Delete your DS Data records in order to add Key Data.
+
+
+
+ {% elif not dnssec_key_confirmed %}
+
In order to enable DNSSEC and add DS records, you must first configure it with your DNS hosting service. Your configuration will determine whether you need to add DS Data or Key Data. Contact your DNS hosting provider if you are unsure which record type to add.
+
+ {% else %}
+
+
Enter the values given by your DNS provider for DS Key Data.