acme-proxy-admin 0.6.1

The operation layer and web admin panel of acme-proxy (internal crate, no semver promise)
Documentation
{#- The account itself, plus everything that acts on it.

    This is the swap target of every account mutation: a contact edit, a
    deactivation and a delete all return this same fragment, so the card an
    operator is looking at is always the state the database is in. -#}
<div id="account-card">
  {% include "partials/_flash.html" %}

  <div class="panel">
    <dl class="fields">
      <dt>Account</dt><dd><code>{{ account.id }}</code></dd>
      <dt>Profile</dt><dd><code>{{ account.profile }}</code></dd>
      <dt>Status</dt><dd><span class="badge {{ account.status }}">{{ account.status }}</span></dd>
      <dt>Public key</dt><dd><code>{{ account.pubkeyFingerprint }}</code></dd>
      <dt>Created</dt><dd>{{ account.createdAt }}</dd>
      {#- The credential the account registered under. It may since have been
          deleted, in which case the link is a 404 -- which is the truth. -#}
      {% if account.eabKid %}
        <dt>EAB credential</dt>
        <dd><a href="/ui/eab/{{ account.eabKid }}"><code>{{ account.eabKid }}</code></a></dd>
      {% endif %}
      {#- Traceability, recorded at `newAccount` and advanced on every
          authenticated request. Every one is rendered only when present: a blank
          `<dd>` under a label would read as "unknown" rather than "never
          recorded". The address and its reverse name share one `<dd>` because a
          name without an address is not a state that exists -- but an address
          without a name is, so the inner test is its own. -#}
      {% if account.createdIp %}
        <dt>Created from</dt>
        <dd><code>{{ account.createdIp }}</code>{% if account.createdPtr %}<br>{{ account.createdPtr }}{% endif %}</dd>
      {% endif %}
      {% if account.lastSeenAt %}<dt>Last seen</dt><dd>{{ account.lastSeenAt }}</dd>{% endif %}
      {% if account.lastSeenIp %}
        <dt>Last seen from</dt>
        <dd><code>{{ account.lastSeenIp }}</code>{% if account.lastSeenPtr %}<br>{{ account.lastSeenPtr }}{% endif %}</dd>
      {% endif %}
      <dt>Orders URL</dt><dd class="wrap-anywhere"><code>{{ account.orders }}</code></dd>
      <dt>Audit trail</dt>
      <dd><a href="/ui/audit?accountId={{ account.id }}">Rows for this account</a></dd>
      {% if account.termsOfServiceAgreed is defined %}
        <dt>Terms agreed</dt><dd>{{ account.termsOfServiceAgreed }}</dd>
      {% endif %}
    </dl>
  </div>

  {#- Hidden below the operator tier; see `pages::fragment_context`. -#}
  {% if can_write %}
  <div class="panel">
    <h2>Contact</h2>
    {#- One `mailto:` URI per line. Validated by the same
        `acme::rules::contact_shape_error` the ACME `newAccount` path and
        the JSON API both call -- the three must not diverge on what a valid
        contact is. -#}
    <form hx-post="/ui/accounts/{{ account.id }}/contact" hx-target="#account-card">
      <div class="field">
        <label for="contact">One URI per line, e.g. <code>mailto:ops@example.com</code></label>
        <textarea id="contact" name="contact">{{ account.contact | default([]) | join("\n") }}</textarea>
      </div>
      <div class="actions">
        <button type="submit" class="primary">Save contact</button>
      </div>
    </form>
  </div>
  {% endif %}

  {#- Hidden below the operator tier; see `pages::fragment_context`. -#}
  {% if can_write %}
  <div class="panel">
    <h2>Danger zone</h2>
    <div class="actions">
      {% if account.status != "deactivated" %}
        <button class="danger"
                hx-post="/ui/accounts/{{ account.id }}/deactivate"
                hx-target="#account-card"
                hx-confirm="Deactivate this account? It will no longer be able to request issuance.">
          Deactivate
        </button>
      {% endif %}
      {#- The delete cascades; the CLI names the count in its confirmation
          prompt, so this one does too. `order_count` and `live_certificates`
          are set by both the page and the fragment every account mutation
          answers with.
          Success answers with a redirect, since the page this button lives on
          is the thing being deleted; a refusal answers with this card, hence
          the target. Disabled while a certificate is live -- the handler
          refuses it anyway, and a button that can only say no is worse. -#}
      <button class="danger"
              hx-delete="/ui/accounts/{{ account.id }}"
              hx-target="#account-card"
              {% if live_certificates %}disabled{% endif %}
              hx-confirm="Delete this account and its {{ order_count }} order(s), with their authorizations and challenges? This cannot be undone.">
        Delete
      </button>
    </div>
    {% if live_certificates %}
      <p class="small">
        Cannot be deleted while it holds {{ live_certificates }} live
        certificate(s) — deleting their orders would leave them impossible to
        revoke. Revoke them first, or wait for them to expire.
      </p>
    {% endif %}
    <p class="muted small">
      Deactivating is the ACME state change and is reversible only by the
      client. Deleting removes the row and cascades to its orders,
      authorizations and challenges, and is refused while any of them holds a
      certificate that is neither revoked nor expired.
    </p>
  </div>
  {% endif %}
</div>