This is an automated email from the ASF dual-hosted git repository.

sbp pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-release.git


The following commit(s) were added to refs/heads/main by this push:
     new 18d3e27  Use structured forms for adding and deleting keys
18d3e27 is described below

commit 18d3e27b1065ce0774810aa9190041ccf18028e7
Author: Sean B. Palmer <[email protected]>
AuthorDate: Wed Apr 2 15:28:19 2025 +0100

    Use structured forms for adding and deleting keys
---
 atr/routes/draft.py                 |   2 +-
 atr/routes/keys.py                  |  65 ++++++++++--
 atr/templates/includes/sidebar.html |   4 +-
 atr/templates/keys-add.html         | 190 +++++++++++++++++++++---------------
 atr/templates/keys-review.html      |  16 ++-
 5 files changed, 183 insertions(+), 94 deletions(-)

diff --git a/atr/routes/draft.py b/atr/routes/draft.py
index f708e2c..508fd24 100644
--- a/atr/routes/draft.py
+++ b/atr/routes/draft.py
@@ -167,7 +167,7 @@ async def add_file(session: routes.CommitterSession, 
project_name: str, version_
 
             await _upload_files(project_name, version_name, file_name, 
file_data)
             return await session.redirect(
-                review, success="File(s) added successfully", 
project_name=project_name, version_name=version_name
+                review, success="File or files added successfully", 
project_name=project_name, version_name=version_name
             )
         except Exception as e:
             logging.exception("Error adding file(s):")
diff --git a/atr/routes/keys.py b/atr/routes/keys.py
index 13c9bb6..9dd86ed 100644
--- a/atr/routes/keys.py
+++ b/atr/routes/keys.py
@@ -35,6 +35,7 @@ import quart
 import werkzeug.datastructures as datastructures
 import werkzeug.wrappers.response as response
 import wtforms
+from wtforms import widgets
 
 import atr.db as db
 import atr.db.models as models
@@ -42,6 +43,10 @@ import atr.routes as routes
 import atr.util as util
 
 
+class DeleteKeyForm(util.QuartFormTyped):
+    submit = wtforms.SubmitField("Delete key")
+
+
 class AddSSHKeyForm(util.QuartFormTyped):
     key = wtforms.StringField("SSH key", widget=wtforms.widgets.TextArea())
     submit = wtforms.SubmitField("Add SSH key")
@@ -247,20 +252,55 @@ async def add(session: routes.CommitterSession) -> str:
         project_list = session.committees + session.projects
         user_committees = await data.committee(name_in=project_list).all()
 
-    if quart.request.method == "POST":
+    committee_choices = [(c.name, c.display_name or c.name) for c in 
user_committees]
+
+    class AddGpgKeyForm(util.QuartFormTyped):
+        public_key = wtforms.TextAreaField(
+            "Public GPG key", 
validators=[wtforms.validators.InputRequired("Public key is required")]
+        )
+        selected_committees = wtforms.SelectMultipleField(
+            "Associate key with committees",
+            validators=[wtforms.validators.InputRequired("You must select at 
least one committee")],
+            coerce=str,
+            choices=committee_choices,
+            option_widget=widgets.CheckboxInput(),
+            widget=widgets.ListWidget(prefix_label=False),
+        )
+        submit = wtforms.SubmitField("Add GPG key")
+
+    form = await AddGpgKeyForm.create_form(data=await quart.request.form if 
quart.request.method == "POST" else None)
+
+    if await form.validate_on_submit():
         try:
-            key_info = await key_add_post(session, quart.request, 
user_committees)
+            public_key_data: str = util.unwrap(form.public_key.data)
+            selected_committees_data: list[str] = 
util.unwrap(form.selected_committees.data)
+
+            invalid_committees = [
+                committee
+                for committee in selected_committees_data
+                if (committee not in session.committees) and (committee not in 
session.projects)
+            ]
+            if invalid_committees:
+                raise routes.FlashError(f"Invalid PMC selection: {', 
'.join(invalid_committees)}")
+
+            key_info = await key_user_add(session.uid, public_key_data, 
selected_committees_data)
+            if key_info:
+                await quart.flash(f"GPG key {key_info.get('fingerprint', '')} 
added successfully.", "success")
+            # Clear form data on success by creating a new empty form instance
+            form = await AddGpgKeyForm.create_form()
+
         except routes.FlashError as e:
-            logging.exception("FlashError:")
+            logging.warning("FlashError adding GPG key: %s", e)
             await quart.flash(str(e), "error")
         except Exception as e:
-            logging.exception("Exception:")
-            await quart.flash(f"Exception: {e}", "error")
+            logging.exception("Exception adding GPG key:")
+            await quart.flash(f"An unexpected error occurred: {e!s}", "error")
 
     return await quart.render_template(
         "keys-add.html",
         asf_id=session.uid,
         user_committees=user_committees,
+        form=form,
         key_info=key_info,
         algorithms=routes.algorithms,
     )
@@ -268,11 +308,15 @@ async def add(session: routes.CommitterSession) -> str:
 
 @routes.committer("/keys/delete", methods=["POST"])
 async def delete(session: routes.CommitterSession) -> response.Response:
-    """Delete a public signing key from the user's account."""
-    form = await routes.get_form(quart.request)
-    fingerprint = form.get("fingerprint")
+    """Delete a public signing key or SSH key from the user's account."""
+    form = await DeleteKeyForm.create_form(data=await quart.request.form)
+
+    if not await form.validate_on_submit():
+        return await session.redirect(review, error="Invalid request for key 
deletion.")
+
+    fingerprint = (await quart.request.form).get("fingerprint")
     if not fingerprint:
-        return await session.redirect(review, error="No key fingerprint 
provided")
+        return await session.redirect(review, error="Missing key fingerprint 
for deletion.")
 
     async with db.session() as data:
         async with data.begin():
@@ -305,6 +349,8 @@ async def review(session: routes.CommitterSession) -> str:
     status_message = quart.request.args.get("status_message")
     status_type = quart.request.args.get("status_type")
 
+    delete_form = await DeleteKeyForm.create_form()
+
     return await quart.render_template(
         "keys-review.html",
         asf_id=session.uid,
@@ -314,6 +360,7 @@ async def review(session: routes.CommitterSession) -> str:
         status_message=status_message,
         status_type=status_type,
         now=datetime.datetime.now(datetime.UTC),
+        delete_form=delete_form,
     )
 
 
diff --git a/atr/templates/includes/sidebar.html 
b/atr/templates/includes/sidebar.html
index e756a6b..86834f4 100644
--- a/atr/templates/includes/sidebar.html
+++ b/atr/templates/includes/sidebar.html
@@ -85,13 +85,13 @@
           <a href="{{ as_url(routes.keys.review) }}">Your signing keys</a>
         </li>
         <li>
-          <a href="{{ as_url(routes.keys.add) }}">Add your signing key</a>
+          <a href="{{ as_url(routes.keys.add) }}">Add your GPG key</a>
         </li>
         <li>
           <a href="{{ as_url(routes.keys.upload) }}">Upload a KEYS file</a>
         </li>
         <li>
-          <a href="{{ as_url(routes.keys.ssh_add) }}">Add SSH key</a>
+          <a href="{{ as_url(routes.keys.ssh_add) }}">Add your SSH key</a>
         </li>
       </ul>
 
diff --git a/atr/templates/keys-add.html b/atr/templates/keys-add.html
index 5df0843..0dd5fbe 100644
--- a/atr/templates/keys-add.html
+++ b/atr/templates/keys-add.html
@@ -1,98 +1,132 @@
 {% extends "layouts/base.html" %}
 
 {% block title %}
-  Add signing key ~ ATR
+  Add your GPG key ~ ATR
 {% endblock title %}
 
 {% block description %}
-  Add a public signing key to your account.
+  Add your public signing key to your ATR account.
 {% endblock description %}
 
 {% block content %}
-  <h1>Add signing key</h1>
-  <p class="intro">Add your public key to use for signing release 
artifacts.</p>
+  <div class="my-4">
+    <h1 class="mb-4">Add your GPG key</h1>
 
-  <div class="user-info">
-    <p>
-      Welcome, <strong>{{ asf_id }}</strong>! You are authenticated as an ASF 
committer.
-    </p>
-  </div>
-
-  {% if key_info %}
-    <h2>Key results</h2>
-    <div class="mt-3 p-3 bg-light rounded">
-      <h3 class="mt-0">Success: Added Key</h3>
-      <dl class="row mb-0">
-        <dt class="col-sm-3 fw-bold">Key ID</dt>
-        <dd class="col-sm-9 mb-2">
-          {{ key_info.key_id }}
-        </dd>
-        <dt class="col-sm-3 fw-bold">Fingerprint</dt>
-        <dd class="col-sm-9 mb-2">
-          {{ key_info.fingerprint }}
-        </dd>
-        <dt class="col-sm-3 fw-bold">User ID</dt>
-        <dd class="col-sm-9 mb-2">
-          {{ key_info.user_id }}
-        </dd>
-        <dt class="col-sm-3 fw-bold">Created</dt>
-        <dd class="col-sm-9 mb-2">
-          {{ key_info.creation_date }}
-        </dd>
-        <dt class="col-sm-3 fw-bold">Expires</dt>
-        <dd class="col-sm-9 mb-2">
-          {{ key_info.expiration_date or 'Never' }}
-        </dd>
-        <dt class="col-sm-3 fw-bold">Key Data</dt>
-        <dd class="col-sm-9 mb-2">
-          <pre class="mb-0">{{ key_info.data }}</pre>
-        </dd>
-      </dl>
-    </div>
-  {% endif %}
+    <p>Add your public key to use for signing release artifacts.</p>
+    {% if form.errors %}<div class="alert alert-danger">Please correct the 
errors below:</div>{% endif %}
 
-  <form method="post" class="striking">
-    <div class="mb-4">
-      <div class="mb-3">
-        <label for="public_key" class="form-label">Public key:</label>
-      </div>
-      <textarea id="public_key"
-                name="public_key"
-                class="form-control mb-2"
-                rows="8"
-                required
-                placeholder="Paste your public key here (in ASCII-armored 
format)"
-                aria-describedby="key-help"></textarea>
-      <small id="key-help" class="form-text text-muted">
-        Your public key should be in ASCII-armored format, starting with 
"-----BEGIN PGP PUBLIC KEY BLOCK-----"
-      </small>
-    </div>
+    <form method="post"
+          class="striking py-4 px-5"
+          action="{{ as_url(routes.keys.add) }}"
+          novalidate>
+      {{ form.hidden_tag() if form.hidden_tag }}
 
-    {% if user_committees %}
       <div class="mb-4">
-        <div class="mb-3">
-          <label class="form-label">Associate with projects:</label>
+        <div class="row mb-3 pb-3 border-bottom{% if form.public_key.errors %} 
has-danger{% endif %}">
+          <div class="col-md-3 text-md-end fw-medium pt-2">{{ 
form.public_key.label }}</div>
+          <div class="col-md-9">
+            {{ form.public_key(class_='form-control font-monospace', rows=10, 
placeholder='Paste your ASCII-armored public GPG key here...') }}
+            <small class="form-text text-muted">
+              Your public key should be in ASCII-armored format, starting with 
"-----BEGIN PGP PUBLIC KEY BLOCK-----"
+            </small>
+            {% if form.public_key.errors %}
+              <div class="invalid-feedback d-block">
+                {% for error in form.public_key.errors %}{{ error }}{% endfor 
%}
+              </div>
+            {% endif %}
+          </div>
         </div>
-        <div class="d-flex flex-wrap gap-3 mb-2">
-          {% for committee in user_committees|sort(attribute='name') %}
-            <div class="form-check d-flex align-items-center gap-2">
-              <input type="checkbox"
-                     class="form-check-input"
-                     id="committee_{{ committee.name }}"
-                     name="selected_committees"
-                     value="{{ committee.name }}" />
-              <label class="form-check-label mb-0" for="committee_{{ 
committee.name }}">{{ committee.display_name }}</label>
+
+        <div class="row mb-3 pb-3 border-bottom{% if 
form.selected_committees.errors %} has-danger{% endif %}">
+          <div class="col-md-3 text-md-end fw-medium pt-2">{{ 
form.selected_committees.label }}</div>
+          <div class="col-md-9">
+            <div class="row">
+              {% for subfield in form.selected_committees %}
+                <div class="col-sm-12 col-md-6 col-lg-4">
+                  <div class="form-check mb-2">
+                    {{ subfield(class_='form-check-input') }}
+                    {{ subfield.label(class_='form-check-label') }}
+                  </div>
+                </div>
+              {% endfor %}
+            </div>
+            <div class="mt-2 mb-2">
+              <button type="button"
+                      id="toggleCommitteesBtn"
+                      class="btn btn-sm btn-outline-secondary">Select 
all</button>
             </div>
-          {% endfor %}
+            <small class="form-text text-muted">Select the committees you are 
a member of.</small>
+            {% if form.selected_committees.errors %}
+              <div class="invalid-feedback d-block">
+                {% for error in form.selected_committees.errors %}{{ error 
}}{% endfor %}
+              </div>
+            {% endif %}
+          </div>
         </div>
-        <small class="form-text text-muted">You must associate your key with 
at least one PMC of which you are a member.</small>
       </div>
-    {% else %}
-      <div class="text-danger mt-1">
-        <p>You must be a member of at least one PMC to add a signing key.</p>
+
+      <div class="mt-4">
+        {{ form.submit(class_='btn btn-primary') }}
+        <a href="{{ as_url(routes.keys.review) }}"
+           class="btn btn-link text-secondary">Cancel</a>
       </div>
-    {% endif %}
+    </form>
 
-    <button type="submit" class="btn btn-primary">Add key</button>
-  </form>
+    {% if key_info and key_info.status == 'success' %}
+      <div class="mt-5">
+        <h2 class="mb-3 fs-5">Key details added:</h2>
+        <div class="p-3 bg-light border rounded">
+          <p>
+            <strong>Key ID:</strong> {{ key_info.key_id }}
+            <br />
+            <strong>Fingerprint:</strong> <code>{{ key_info.fingerprint 
}}</code>
+            <br />
+            <strong>User ID:</strong> {{ key_info.user_id }}
+            <br />
+            <strong>Created:</strong> {{ 
key_info.creation_date.strftime("%Y-%m-%d") }}
+            <br />
+            <strong>Expires:</strong> {{ 
key_info.expiration_date.strftime("%Y-%m-%d") if key_info.expiration_date else 
'Never' }}
+          </p>
+        </div>
+      </div>
+    {% endif %}
+  </div>
 {% endblock content %}
+
+{% block javascripts %}
+  {{ super() }}
+  <script>
+      document.addEventListener("DOMContentLoaded", () => {
+          const btn = document.getElementById("toggleCommitteesBtn");
+          const checkboxes = 
document.querySelectorAll("input[name='selected_committees']");
+
+          if (!btn || checkboxes.length === 0) return;
+
+          function updateButtonText() {
+              let allChecked = true;
+              checkboxes.forEach(cb => {
+                  if (!cb.checked) allChecked = false;
+              });
+              btn.textContent = allChecked ? "Select none" : "Select all";
+          }
+
+          btn.addEventListener("click", () => {
+              let allChecked = true;
+              checkboxes.forEach(cb => {
+                  if (!cb.checked) allChecked = false;
+              });
+              const shouldCheck = !allChecked;
+              checkboxes.forEach(cb => {
+                  cb.checked = shouldCheck;
+              });
+              updateButtonText();
+          });
+
+          checkboxes.forEach(cb => {
+              cb.addEventListener("change", updateButtonText);
+          });
+
+          updateButtonText();
+      });
+  </script>
+{% endblock javascripts %}
diff --git a/atr/templates/keys-review.html b/atr/templates/keys-review.html
index 41fbbb0..baefb3a 100644
--- a/atr/templates/keys-review.html
+++ b/atr/templates/keys-review.html
@@ -83,9 +83,13 @@
               <pre class="mt-3">{{ key.ascii_armored_key }}</pre>
             </details>
 
-            <form method="post" action="{{ as_url(routes.keys.delete) }}" 
class="mt-3">
+            <form method="post"
+                  action="{{ as_url(routes.keys.delete) }}"
+                  class="mt-3"
+                  onsubmit="return confirm('Are you sure you want to delete 
this GPG key?');">
+              {{ delete_form.hidden_tag() }}
               <input type="hidden" name="fingerprint" value="{{ 
key.fingerprint }}" />
-              <button type="submit" class="btn btn-danger">Delete key</button>
+              {{ delete_form.submit(class_='btn btn-danger', value='Delete 
key') }}
             </form>
           </div>
         {% endfor %}
@@ -125,9 +129,13 @@
               <pre class="mt-3">{{ key.key }}</pre>
             </details>
 
-            <form method="post" action="{{ as_url(routes.keys.delete) }}" 
class="mt-3">
+            <form method="post"
+                  action="{{ as_url(routes.keys.delete) }}"
+                  class="mt-3"
+                  onsubmit="return confirm('Are you sure you want to delete 
this SSH key?');">
+              {{ delete_form.hidden_tag() }}
               <input type="hidden" name="fingerprint" value="{{ 
key.fingerprint }}" />
-              <button type="submit" class="btn btn-danger">Delete key</button>
+              {{ delete_form.submit(class_='btn btn-danger', value='Delete 
key') }}
             </form>
           </div>
         {% endfor %}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to