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:
obervinov
2026-06-24 19:15:31 +04:00
parent 9641e169f2
commit 4273c541c5
23 changed files with 1505 additions and 6 deletions
+37 -1
View File
@@ -4,13 +4,19 @@ from django.contrib.auth.forms import (
UserCreationForm,
AdminPasswordChangeForm,
)
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django.contrib import admin
from django.contrib.auth.admin import GroupAdmin as BaseGroupAdmin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth.models import Group
from apps.users.models import User, UserSettings
from apps.users.models import APIToken, User, UserSettings
@admin.action(description=_("Revoke selected API tokens"))
def revoke_api_tokens(modeladmin, request, queryset):
queryset.update(revoked_at=timezone.now())
admin.site.unregister(Group)
@@ -77,3 +83,33 @@ class GroupAdmin(BaseGroupAdmin, ModelAdmin):
admin.site.register(UserSettings)
@admin.register(APIToken)
class APITokenAdmin(admin.ModelAdmin):
actions = [revoke_api_tokens]
list_display = (
"name",
"user",
"token_key",
"created_at",
"last_used_at",
"expires_at",
"revoked_at",
)
search_fields = ("name", "user__email", "token_key")
# Never expose the secret hash in the form; it must not be editable.
exclude = ("token_hash",)
readonly_fields = (
"user",
"name",
"token_key",
"created_at",
"updated_at",
"last_used_at",
"expires_at",
"revoked_at",
)
def has_add_permission(self, request):
return False