mirror of
https://github.com/eitchtee/WYGIWYH.git
synced 2026-07-05 04:21:43 +02:00
Add API tokens and OAuth2 client support for external integrations
- Personal API tokens (model, user-settings UI, admin, management command, DRF auth class) for non-interactive API access from automations like n8n. Raw token shown once; only a SHA-256 hash is stored; last_used_at writes are throttled. - OAuth2 authorization server via django-oauth-toolkit with authorization server metadata and optional, off-by-default Dynamic Client Registration (RFC 7591), so remote OAuth/MCP clients can authenticate and self-register. - Tests for token auth, DCR gating and the management commands, plus .env.example and README documentation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
{% load crispy_forms_tags %}
|
||||
{% load i18n %}
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="text-lg font-bold font-mono">{% translate "API Tokens" %}</div>
|
||||
<p class="text-sm opacity-70">
|
||||
{% translate "Use these tokens for automations such as n8n. The raw token is shown only once after creation." %}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{% if raw_token %}
|
||||
<div role="alert" class="alert alert-warning mb-4">
|
||||
<div class="w-full">
|
||||
<div class="font-semibold mb-1">{% translate "Copy this token now" %}</div>
|
||||
<p class="text-sm opacity-80 mb-3">
|
||||
{% translate "It will not be shown again after this response." %}
|
||||
</p>
|
||||
<div class="join w-full">
|
||||
<input id="raw-token-value"
|
||||
type="text"
|
||||
readonly
|
||||
value="{{ raw_token }}"
|
||||
class="input input-sm join-item w-full font-mono text-xs"
|
||||
_="on focus call me.select()" />
|
||||
<button type="button"
|
||||
class="btn btn-sm btn-primary join-item"
|
||||
_="on click call navigator.clipboard.writeText('{{ raw_token|escapejs }}')
|
||||
then put 'Copied!' into me
|
||||
then wait 1.5s
|
||||
then put 'Copy' into me">
|
||||
{% translate "Copy" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form hx-post="{% url 'user_api_token_add' %}" hx-target="#api-token-settings" hx-swap="innerHTML" novalidate>
|
||||
{% crispy api_token_form %}
|
||||
</form>
|
||||
|
||||
{% if api_tokens %}
|
||||
<div class="overflow-x-auto mt-4">
|
||||
<table class="table table-zebra">
|
||||
<tbody>
|
||||
{% for token in api_tokens %}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<span class="font-medium font-mono">{{ token.name }}</span>
|
||||
{% if token.revoked_at %}
|
||||
<span class="badge badge-sm badge-ghost">{% translate "Revoked" %}</span>
|
||||
{% else %}
|
||||
<span class="badge badge-sm badge-success">{% translate "Active" %}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="text-xs opacity-60 font-mono break-all mt-1">{{ token.token_key }}</div>
|
||||
<div class="text-xs opacity-60">
|
||||
{% blocktranslate with created=token.created_at|date:"Y-m-d" %}Created {{ created }}{% endblocktranslate %}
|
||||
·
|
||||
{% if token.last_used_at %}
|
||||
{% blocktranslate with used=token.last_used_at|date:"Y-m-d H:i" %}last used {{ used }}{% endblocktranslate %}
|
||||
{% else %}
|
||||
{% translate "never used" %}
|
||||
{% endif %}
|
||||
·
|
||||
{% if token.expires_at %}
|
||||
{% blocktranslate with expires=token.expires_at|date:"Y-m-d" %}expires {{ expires }}{% endblocktranslate %}
|
||||
{% else %}
|
||||
{% translate "no expiry" %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
<td class="table-col-auto text-right align-top">
|
||||
{% if not token.revoked_at %}
|
||||
<a class="btn btn-error btn-sm"
|
||||
role="button"
|
||||
data-tippy-content="{% translate 'Revoke' %}"
|
||||
hx-delete="{% url 'user_api_token_revoke' token_id=token.id %}"
|
||||
hx-target="#api-token-settings"
|
||||
hx-swap="innerHTML"
|
||||
hx-trigger="confirmed"
|
||||
data-bypass-on-ctrl="true"
|
||||
data-title="{% translate 'Revoke token?' %}"
|
||||
data-text="{% translate 'This token will stop working immediately.' %}"
|
||||
data-confirm-text="{% translate 'Yes, revoke it!' %}"
|
||||
_="install prompt_swal">
|
||||
<i class="fa-solid fa-ban fa-fw"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="mt-4">
|
||||
<c-msg.empty title="{% translate "No API tokens" %}" remove-padding></c-msg.empty>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -8,4 +8,8 @@
|
||||
<form hx-post="{% url 'user_settings' %}" hx-target="#generic-offcanvas" novalidate>
|
||||
{% crispy form %}
|
||||
</form>
|
||||
<div class="divider my-6"></div>
|
||||
<div id="api-token-settings">
|
||||
{% include "users/fragments/api_tokens.html" %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user