- New endpoints: GET /api/v1/pools, GET /api/v1/pools/{slug}/next,
GET/POST /api/v1/allocations, GET /api/v1/allocations/{id},
POST /api/v1/allocations/{id}/release
- Bearer token auth; only SHA-256 hashes stored, plaintext shown once
- Token management GUI at /tokens (create, revoke, delete) linked in footer
- API actions audited with actor api:<token name>
- Fix latent transaction leak: replace manual BEGIN IMMEDIATE/COMMIT with
begin_with(), which rolls back on drop; previously a failed allocation
(e.g. disallowed prefix or full pool) returned a connection with an open
transaction to the pool, causing 'database is locked' on later writes
- README: API documentation with curl examples
120 lines
4.5 KiB
HTML
120 lines
4.5 KiB
HTML
{% extends "base.html" %}
|
||
|
||
{% block title %}API tokens | SUB-ALIGATOR{% endblock %}
|
||
|
||
{% block content %}
|
||
{% if has_new_token %}
|
||
<section class="box">
|
||
<div class="box-title">Token created – copy it now</div>
|
||
<div class="message success">
|
||
<p>The new API token <strong>{{ new_token_name }}</strong> was created. Copy it now – <strong>it will not be shown again</strong>. Only its hash is stored.</p>
|
||
<input type="text" readonly value="{{ new_token_value }}" style="font-family: inherit;">
|
||
<p class="small" style="margin-top: 0.5rem;">Use it as: <code>Authorization: Bearer {{ new_token_value }}</code></p>
|
||
</div>
|
||
</section>
|
||
{% endif %}
|
||
|
||
<section class="box">
|
||
<div class="box-title">API tokens</div>
|
||
|
||
{% if error != "" %}
|
||
<div class="message error">
|
||
<p>{{ error }}</p>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<p class="small">Tokens authenticate the JSON REST API under <code>/api/v1/</code> via the <code>Authorization: Bearer</code> header. See the README for endpoint documentation.</p>
|
||
|
||
{% if tokens.len() == 0 %}
|
||
<p>No API tokens yet.</p>
|
||
{% else %}
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>Name</th>
|
||
<th>Token</th>
|
||
<th>Status</th>
|
||
<th>Created</th>
|
||
<th>Last used</th>
|
||
<th>Actions</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for t in tokens %}
|
||
<tr>
|
||
<td data-label="Name">{{ t.name }}</td>
|
||
<td data-label="Token"><code>{{ t.token_prefix }}…</code></td>
|
||
<td data-label="Status">
|
||
{% if t.enabled %}
|
||
<span class="status active">active</span>
|
||
{% else %}
|
||
<span class="status released">revoked</span>
|
||
{% endif %}
|
||
</td>
|
||
<td data-label="Created">{{ t.created_at_local }} ({{ t.created_by }})</td>
|
||
<td data-label="Last used">{{ t.last_used_at_local }}</td>
|
||
<td data-label="Actions">
|
||
{% if t.enabled %}
|
||
<button type="button" class="btn btn-danger" data-modal-open="revoke-token-modal-{{ t.id }}">Revoke</button>
|
||
{% else %}
|
||
<button type="button" class="btn btn-danger" data-modal-open="delete-token-modal-{{ t.id }}">Delete</button>
|
||
{% endif %}
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
{% endif %}
|
||
</section>
|
||
|
||
<section class="box">
|
||
<div class="box-title">Create new token</div>
|
||
|
||
<form method="post" action="/tokens/new">
|
||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||
|
||
<label for="name">Token name *</label>
|
||
<input type="text" id="name" name="name" required minlength="2" maxlength="100" placeholder="e.g. monitoring-integration">
|
||
<p class="small">A descriptive name of the integration that will use this token. It appears in the audit log as actor <code>api:<name></code>.</p>
|
||
|
||
<div style="margin-top: 1rem;">
|
||
<button type="submit" class="btn">Create token</button>
|
||
<a href="/" class="btn btn-secondary">Back</a>
|
||
</div>
|
||
</form>
|
||
</section>
|
||
|
||
{% for t in tokens %}
|
||
{% if t.enabled %}
|
||
<div id="revoke-token-modal-{{ t.id }}" class="modal-overlay" style="display:none;">
|
||
<div class="modal">
|
||
<h2 class="modal-title">Revoke API token</h2>
|
||
<form method="post" action="/tokens/{{ t.id }}/revoke" class="modal-body">
|
||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||
<p>Token: <strong>{{ t.name }}</strong> (<code>{{ t.token_prefix }}…</code>)</p>
|
||
<p>The token will stop working immediately. The record is kept for audit purposes.</p>
|
||
<div class="modal-actions">
|
||
<button type="button" class="btn btn-secondary cancel-btn" data-modal="revoke-token-modal-{{ t.id }}">Cancel</button>
|
||
<button type="submit" class="btn btn-danger">Revoke token</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
{% else %}
|
||
<div id="delete-token-modal-{{ t.id }}" class="modal-overlay" style="display:none;">
|
||
<div class="modal">
|
||
<h2 class="modal-title">Delete API token</h2>
|
||
<form method="post" action="/tokens/{{ t.id }}/delete" class="modal-body">
|
||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||
<p>Token: <strong>{{ t.name }}</strong> (<code>{{ t.token_prefix }}…</code>)</p>
|
||
<p>The revoked token record will be permanently deleted. This cannot be undone.</p>
|
||
<div class="modal-actions">
|
||
<button type="button" class="btn btn-secondary cancel-btn" data-modal="delete-token-modal-{{ t.id }}">Cancel</button>
|
||
<button type="submit" class="btn btn-danger">Delete token</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
{% endfor %}
|
||
{% endblock %}
|