diff --git a/CHANGELOG.md b/CHANGELOG.md index 651b123..6520e9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,28 +8,6 @@ the git tags (`vX.Y.Z`) and `daybook_project.__version__`. _Nothing yet._ -## [0.2.0] — 2026-09-04 - -### Added -- **Multiple named API keys per user.** Each user can now hold several keys — - one per agent or script — named and revocable individually, managed from a - new **Settings → API Keys** page (a table with name, key, created, last-used, - and per-row revoke). Keys are viewable on demand (`dbk_`-prefixed). -- **Gated key-provisioning API** for automated test installations: - `GET/POST /api/keys` and `DELETE /api/keys/{id}`, controlled by - `DAYBOOK_KEY_PROVISIONING_API` (unset → follows `DJANGO_DEBUG`, so **off in - deployment**). Advertised as `key_provisioning_api` in `/api/capabilities`; - returns 403 when disabled. -- API keys track `last_used_at` (throttled to one write/minute) so stale keys - are visible before you revoke them. - -### Changed -- Auth now resolves bearer tokens against the new `ApiKey` model instead of the - single DRF `authtoken.Token`. Existing tokens are migrated 1:1 into named - ("default") keys with the same value — no client reconfiguration needed. -- Settings → Account no longer manages a single token; it links to API Keys. - Creating a user (Settings → Users) issues an initial named key. - ## [0.1.1] — 2026-09-03 ### Fixed @@ -85,7 +63,6 @@ First tagged release. - Dockerised (Gunicorn + whitenoise), SQLite, `git`-based deploy with a runbook (`deploy/cloud_agent_deploy.md`) and health check. -[Unreleased]: https://git.cygnus.systems/peter/daybook/compare/v0.2.0...HEAD -[0.2.0]: https://git.cygnus.systems/peter/daybook/compare/v0.1.1...v0.2.0 +[Unreleased]: https://git.cygnus.systems/peter/daybook/compare/v0.1.1...HEAD [0.1.1]: https://git.cygnus.systems/peter/daybook/compare/v0.1.0...v0.1.1 [0.1.0]: https://git.cygnus.systems/peter/daybook/releases/tag/v0.1.0 diff --git a/artifacts/migrations/0014_apikey.py b/artifacts/migrations/0014_apikey.py deleted file mode 100644 index 09e31ce..0000000 --- a/artifacts/migrations/0014_apikey.py +++ /dev/null @@ -1,31 +0,0 @@ -# Generated by Django 5.2.15 on 2026-09-04 09:35 - -import artifacts.models -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('artifacts', '0013_artifact_share_token'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='ApiKey', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=100)), - ('key', models.CharField(db_index=True, default=artifacts.models.generate_api_key, max_length=64, unique=True)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('last_used_at', models.DateTimeField(blank=True, null=True)), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='api_keys', to=settings.AUTH_USER_MODEL)), - ], - options={ - 'ordering': ['created_at'], - }, - ), - ] diff --git a/artifacts/migrations/0015_migrate_tokens_to_apikeys.py b/artifacts/migrations/0015_migrate_tokens_to_apikeys.py deleted file mode 100644 index 46bb6e7..0000000 --- a/artifacts/migrations/0015_migrate_tokens_to_apikeys.py +++ /dev/null @@ -1,32 +0,0 @@ -from django.db import migrations - - -def copy_tokens_to_apikeys(apps, schema_editor): - """Carry every existing DRF authtoken over to a named ApiKey so keys issued - before the multi-key feature keep authenticating (incl. the permanent test - user and any deployed agents). Same key string -> no client reconfiguration.""" - Token = apps.get_model("authtoken", "Token") - ApiKey = apps.get_model("artifacts", "ApiKey") - for token in Token.objects.all(): - if ApiKey.objects.filter(key=token.key).exists(): - continue - api_key = ApiKey.objects.create(user_id=token.user_id, name="default", key=token.key) - # created_at is auto_now_add, so set it after the fact to preserve the - # original issue date (queryset.update bypasses auto_now_add). - ApiKey.objects.filter(pk=api_key.pk).update(created_at=token.created) - - -def noop(apps, schema_editor): - pass - - -class Migration(migrations.Migration): - - dependencies = [ - ("artifacts", "0014_apikey"), - ("authtoken", "0004_alter_tokenproxy_options"), - ] - - operations = [ - migrations.RunPython(copy_tokens_to_apikeys, noop), - ] diff --git a/artifacts/models.py b/artifacts/models.py index e77f143..446454f 100644 --- a/artifacts/models.py +++ b/artifacts/models.py @@ -1,14 +1,7 @@ -import secrets - from django.conf import settings from django.db import models -def generate_api_key(): - """`dbk_` prefix (recognizable as a Daybook key) + 40 hex chars.""" - return "dbk_" + secrets.token_hex(20) - - class Artifact(models.Model): HTML_FRAGMENT = "html_fragment" HTML_FULL = "html_full" @@ -99,28 +92,6 @@ class UserProfile(models.Model): return f"{self.user} quota={self.quota_bytes}" -class ApiKey(models.Model): - """A named, per-user API key for ingest. A user can hold several — one per - agent or script — and revoke them individually. Stored and shown in - plaintext: this is a self-hosted, single-operator app and keys are - viewable on demand by design. - """ - - user = models.ForeignKey( - settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="api_keys" - ) - name = models.CharField(max_length=100) - key = models.CharField(max_length=64, unique=True, default=generate_api_key, db_index=True) - created_at = models.DateTimeField(auto_now_add=True) - last_used_at = models.DateTimeField(null=True, blank=True) - - class Meta: - ordering = ["created_at"] - - def __str__(self): - return f"{self.name} ({self.user})" - - class Asset(models.Model): """An image uploaded for embedding inside an artifact's content. diff --git a/artifacts/urls.py b/artifacts/urls.py index 1cbd657..dae96dc 100644 --- a/artifacts/urls.py +++ b/artifacts/urls.py @@ -15,13 +15,10 @@ urlpatterns = [ path("api/assets", views.AssetUploadView.as_view(), name="asset-upload-api"), path("api/capabilities", views.CapabilitiesView.as_view(), name="capabilities-api"), path("api/skill", views.SkillView.as_view(), name="skill-api"), - path("api/keys", views.ApiKeyListCreateView.as_view(), name="api-keys"), - path("api/keys/", views.ApiKeyDetailView.as_view(), name="api-key-detail"), path("api/artifacts", views.ArtifactListCreateView.as_view(), name="artifact-api"), path("api/artifacts/", views.ArtifactDetailView.as_view(), name="artifact-detail-api"), path("settings/", views.settings_home, name="settings-home"), path("settings/account/", views.settings_account, name="settings-account"), - path("settings/api-keys/", views.settings_api_keys, name="settings-api-keys"), path("settings/users/", views.settings_users, name="settings-users"), path("settings/demo-data/", views.settings_demo_data, name="settings-demo-data"), path("settings/storage/", views.settings_storage, name="settings-storage"), diff --git a/artifacts/views.py b/artifacts/views.py index aa3bfd0..a6ec867 100644 --- a/artifacts/views.py +++ b/artifacts/views.py @@ -29,13 +29,14 @@ from django.utils import timezone from django.utils.decorators import method_decorator from django.views.decorators.clickjacking import xframe_options_sameorigin from django.views.decorators.http import require_POST +from rest_framework.authtoken.models import Token from rest_framework.parsers import FormParser, JSONParser, MultiPartParser from rest_framework.response import Response from rest_framework.views import APIView from .demo_data import create_demo_artifacts from .forms import DaybookLoginForm -from .models import ApiKey, Artifact, Asset, TagColor +from .models import Artifact, Asset, TagColor from .storage import ( QuotaExceeded, SIDECAR_SUFFIXES, @@ -308,17 +309,6 @@ class CapabilitiesView(APIView): return HttpResponseForbidden("Login or a valid API token is required") from daybook_project import __version__ - methods = { - "/api/artifacts": ["GET", "POST"], - "/api/artifacts/{id}": ["GET", "PATCH", "DELETE"], - "/api/assets": ["POST"], - "/api/capabilities": ["GET"], - "/api/skill": ["GET"], - } - if settings.DAYBOOK_KEY_PROVISIONING_API: - methods["/api/keys"] = ["GET", "POST"] - methods["/api/keys/{id}"] = ["DELETE"] - return Response( { "version": __version__, @@ -328,9 +318,14 @@ class CapabilitiesView(APIView): "raw_supported_for": [Artifact.MARKDOWN, Artifact.HTML_FULL, Artifact.JSON, Artifact.CSV], "source_types": [choice[0] for choice in Artifact.SOURCE_TYPE_CHOICES], "auth": "bearer_token", - "methods": methods, + "methods": { + "/api/artifacts": ["GET", "POST"], + "/api/artifacts/{id}": ["GET", "PATCH", "DELETE"], + "/api/assets": ["POST"], + "/api/capabilities": ["GET"], + "/api/skill": ["GET"], + }, "skill_url": request.build_absolute_uri(reverse("skill-api")), - "key_provisioning_api": settings.DAYBOOK_KEY_PROVISIONING_API, } ) @@ -378,71 +373,6 @@ class SkillView(APIView): return HttpResponse(skill, content_type="text/markdown; charset=utf-8") -def serialize_api_key(api_key): - return { - "id": api_key.pk, - "name": api_key.name, - "key": api_key.key, - "created_at": api_key.created_at.isoformat(), - "last_used_at": api_key.last_used_at.isoformat() if api_key.last_used_at else None, - } - - -class ApiKeyListCreateView(APIView): - """Programmatic API-key management, for automated test installations that - provision their own agents. Gated by settings.DAYBOOK_KEY_PROVISIONING_API, - which is OFF in deployment (it hands out key material over the API). When - off, every method returns 403. Authenticated by session or an existing - bearer token, and only ever operates on that user's own keys. - - GET /api/keys -> list this user's keys (incl. key values) - POST /api/keys -> create {"name": ...} -> the new key - """ - - def _resolve_user(self, request): - if request.user.is_authenticated: - return request.user - return resolve_token_user(request) - - def get(self, request): - if not settings.DAYBOOK_KEY_PROVISIONING_API: - return Response({"detail": "Key provisioning API is disabled on this instance."}, status=403) - user = self._resolve_user(request) - if user is None: - return HttpResponseForbidden("Login or a valid API token is required") - return Response([serialize_api_key(k) for k in user.api_keys.all()]) - - def post(self, request): - if not settings.DAYBOOK_KEY_PROVISIONING_API: - return Response({"detail": "Key provisioning API is disabled on this instance."}, status=403) - user = self._resolve_user(request) - if user is None: - return HttpResponseForbidden("Login or a valid API token is required") - name = (request.data.get("name") or "").strip() - if not name: - return Response({"detail": "A non-empty 'name' is required."}, status=400) - api_key = ApiKey.objects.create(user=user, name=name) - return Response(serialize_api_key(api_key), status=201) - - -class ApiKeyDetailView(APIView): - """Revoke a single key by id, same gating and auth as the list/create view. - - DELETE /api/keys/{id} - """ - - def delete(self, request, key_id): - if not settings.DAYBOOK_KEY_PROVISIONING_API: - return Response({"detail": "Key provisioning API is disabled on this instance."}, status=403) - user = request.user if request.user.is_authenticated else resolve_token_user(request) - if user is None: - return HttpResponseForbidden("Login or a valid API token is required") - deleted, _ = ApiKey.objects.filter(pk=key_id, user=user).delete() - if not deleted: - raise Http404("No such key") - return Response(status=204) - - FEED_WINDOW_DAYS = 7 @@ -1062,15 +992,9 @@ def resolve_token_user(request): if not key: return None try: - api_key = ApiKey.objects.select_related("user").get(key=key) - except ApiKey.DoesNotExist: + return Token.objects.select_related("user").get(key=key).user + except Token.DoesNotExist: return None - # Stamp last-used so stale keys are visible in the UI, but throttle the - # write to at most once a minute to avoid a DB write on every request. - now = timezone.now() - if api_key.last_used_at is None or (now - api_key.last_used_at).total_seconds() > 60: - ApiKey.objects.filter(pk=api_key.pk).update(last_used_at=now) - return api_key.user def normalize_tags(tags): @@ -1326,36 +1250,16 @@ def settings_home(request): @login_required def settings_account(request): + token, _ = Token.objects.get_or_create(user=request.user) + if request.method == "POST" and request.POST.get("action") == "regenerate": + token.delete() + token = Token.objects.create(user=request.user) + messages.success(request, "API token regenerated.") + return redirect("settings-account") return render( request, "artifacts/settings_account.html", - {"active_section": "account", "key_count": request.user.api_keys.count()}, - ) - - -@login_required -def settings_api_keys(request): - if request.method == "POST": - action = request.POST.get("action") - if action == "create": - name = (request.POST.get("name") or "").strip() - if not name: - messages.error(request, "Give the key a name so you know which agent it's for.") - else: - ApiKey.objects.create(user=request.user, name=name) - messages.success(request, f"Created API key '{name}'.") - elif action == "revoke": - key_id = request.POST.get("key_id") or "" - deleted, _ = ApiKey.objects.filter(pk=key_id, user=request.user).delete() - if deleted: - messages.success(request, "API key revoked.") - else: - messages.error(request, "No such key.") - return redirect("settings-api-keys") - return render( - request, - "artifacts/settings_api_keys.html", - {"active_section": "api-keys", "api_keys": request.user.api_keys.all()}, + {"token": token, "active_section": "account"}, ) @@ -1365,7 +1269,7 @@ def settings_users(request): return HttpResponseForbidden("Admin access required") created_user = None - created_key = None + created_token = None if request.method == "POST": username = request.POST.get("username", "").strip() password = request.POST.get("password", "") @@ -1379,7 +1283,7 @@ def settings_users(request): if grant_admin: created_user.is_superuser = True created_user.save(update_fields=["is_superuser"]) - created_key = ApiKey.objects.create(user=created_user, name="default") + created_token = Token.objects.create(user=created_user) messages.success(request, f"Created user '{username}'.") return render( @@ -1389,7 +1293,7 @@ def settings_users(request): "active_section": "users", "users": User.objects.order_by("username"), "created_user": created_user, - "created_key": created_key, + "created_token": created_token, }, ) diff --git a/daybook_project/__init__.py b/daybook_project/__init__.py index b5b7a6a..e27f5fc 100644 --- a/daybook_project/__init__.py +++ b/daybook_project/__init__.py @@ -1,4 +1,4 @@ # Single source of truth for the app version. Bump this, commit, then tag the # commit `vX.Y.Z` and deploy. Surfaced on Settings -> About and in # GET /api/capabilities ("version"). -__version__ = "0.2.0" +__version__ = "0.1.1" diff --git a/daybook_project/settings.py b/daybook_project/settings.py index c8987a1..ee85175 100644 --- a/daybook_project/settings.py +++ b/daybook_project/settings.py @@ -9,17 +9,6 @@ SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY", "dev-only-change-me") DEBUG = os.environ.get("DJANGO_DEBUG", "1").lower() in {"1", "true", "yes", "on"} ALLOWED_HOSTS = [host.strip() for host in os.environ.get("DJANGO_ALLOWED_HOSTS", "*").split(",") if host.strip()] -# Allow creating/listing/revoking API keys over the ingest API (POST /api/keys). -# Meant for automated test installations that provision their own agents. It -# exposes key material over the API, so it is OFF in deployment. Unset, it -# follows DEBUG (on locally, off in production); set DAYBOOK_KEY_PROVISIONING_API -# explicitly (0/1) to override either way. -_key_api_env = os.environ.get("DAYBOOK_KEY_PROVISIONING_API") -if _key_api_env is None: - DAYBOOK_KEY_PROVISIONING_API = DEBUG -else: - DAYBOOK_KEY_PROVISIONING_API = _key_api_env.lower() in {"1", "true", "yes", "on"} - # Needed when running behind a TLS-terminating reverse proxy (Apache/Nginx/etc.): # Django otherwise thinks every request is plain HTTP, which breaks CSRF checks # on login/POST. The proxy must forward X-Forwarded-Proto: https. diff --git a/deploy/env.example b/deploy/env.example index 1eb6d11..5fe9ca3 100644 --- a/deploy/env.example +++ b/deploy/env.example @@ -6,11 +6,6 @@ DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1,your-vm-ip,your-domain.example # "CSRF verification failed" because Django can't tell the original request # was HTTPS. Set to the public HTTPS origin(s) users actually visit: DJANGO_CSRF_TRUSTED_ORIGINS=https://your-domain.example -# Leave OFF in deployment. When on, POST /api/keys can create/list/revoke API -# keys over the network (it returns key material), which is only appropriate -# for automated test installations. Unset, it follows DJANGO_DEBUG — so with -# DJANGO_DEBUG=0 above it is already off. Set to 1 only on a throwaway test box. -# DAYBOOK_KEY_PROVISIONING_API=0 DAYBOOK_DATA_DIR=/data DATABASE_PATH=/data/db.sqlite3 ARTIFACTS_ROOT=/data/artifacts_store diff --git a/docs/agent_quickstart.md b/docs/agent_quickstart.md index 0c78356..d6ba619 100644 --- a/docs/agent_quickstart.md +++ b/docs/agent_quickstart.md @@ -2,11 +2,10 @@ This is the shortest path for an agent to publish and retrieve artifacts from Daybook. -## 0. Get Your API Key +## 0. Get Your API Token -Log in to the app in a browser and create a personal key under **Settings → API Keys** -(`http://127.0.0.1:8001/settings/api-keys/`). Name it after the agent so you can revoke it -later. Every artifact you post with it lands in your +Log in to the app in a browser and copy your personal token from **Settings → Account** +(`http://127.0.0.1:8001/settings/account/`). Every artifact you post with it lands in your feed, and only yours. ## 1. Check Capabilities diff --git a/docs/api.md b/docs/api.md index 581f8f5..b3778c2 100644 --- a/docs/api.md +++ b/docs/api.md @@ -8,13 +8,13 @@ Base URL while running locally: http://127.0.0.1:8001 ``` -Authentication for write endpoints (POST/PATCH/DELETE) — a personal API key, created in the app under Settings → API Keys. Each user can hold several (one per agent/script) and revoke them individually: +Authentication for write endpoints (POST/PATCH) — a personal per-user API token, from Settings → Account in the app: ```http -Authorization: Bearer +Authorization: Bearer ``` -Every artifact belongs to the user whose key authenticated the POST — feeds are private per user. +Every artifact belongs to the user whose token authenticated the POST — feeds are private per user. Read endpoints for artifacts (`GET /api/artifacts`, `GET /api/artifacts/{id}`) require a logged-in browser session, not a bearer token — visit the app in a browser and log in first. @@ -575,40 +575,9 @@ curl -H "Authorization: Bearer " http://127.0.0.1:8001/api/ Discover it programmatically via `skill_url` in the capabilities response. -## API Keys (provisioning) - -Keys are normally created and revoked in the app under **Settings → API Keys**. -For automated test installations, a programmatic surface can provision them -too — but it is **gated** and **off in deployment**, because it returns key -material over the API. - -The gate is `DAYBOOK_KEY_PROVISIONING_API`. Unset, it follows `DJANGO_DEBUG` -(on locally, off in production); set it explicitly (`0`/`1`) to override. -`GET /api/capabilities` reports the current state as `key_provisioning_api`, -and only lists the `/api/keys` methods when it's enabled. When disabled, every -method below returns `403`. - -```http -GET /api/keys # list this user's keys (incl. key values) -POST /api/keys # body {"name": "hermes-agent"} -> the new key -DELETE /api/keys/{id} # revoke one key -``` - -Authenticated by a browser session **or** an existing bearer key, and only -ever operates on the authenticated user's own keys. - -```bash -# Create a new named key (test installations only) -curl -X POST -H "Authorization: Bearer " \ - -H "Content-Type: application/json" \ - -d '{"name": "hermes-agent"}' \ - http://127.0.0.1:8001/api/keys -``` - ## Current MVP Constraints -- Each user can hold several personal API keys (Settings → API Keys), each - individually revocable; there is no shared key. +- Each user has a personal ingest API token; there is no shared token. - Reader/browser access requires login; feeds are private per user (except per-artifact public share links — see Sharing above). - Content modification/revisions are deferred (`PATCH` edits metadata only, diff --git a/docs/deployment.md b/docs/deployment.md index cf83192..aafbfad 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -64,10 +64,9 @@ DATABASE_PATH=/data/db.sqlite3 ARTIFACTS_ROOT=/data/artifacts_store ``` -There is no shared API token to configure — Daybook uses per-user API keys -created under Settings → API Keys once a user exists (each user can hold -several, one per agent, each individually revocable). Do not commit -`deploy/.env`. +There is no shared API token to configure — Daybook uses per-user tokens +(DRF `Token` model) issued from Settings → Account once a user exists. Do not +commit `deploy/.env`. Create the persistent data directory and bring the stack up: @@ -95,7 +94,7 @@ that's expected; it confirms the app is up and enforcing auth. (Don't add `curl -f` — it treats the 403 as an error. The container healthcheck uses the login page, which is 200 without auth.) Open `http://:8000/` in a browser and log in with the superuser you just -created, then create a personal key under Settings → API Keys and use it for a +created, then grab a personal token from Settings → Account and use it for a smoke-test post: ```bash @@ -192,8 +191,8 @@ Caddy: header_up X-Forwarded-Proto https - `docker compose -f deploy/docker-compose.yml ps` shows `daybook` healthy. - `GET /api/capabilities` returns 403 without auth, 200 with a valid token or session (check the status code — don't use `curl -f`, which errors on 403). -- At least one superuser exists (`manage.py createsuperuser`) with a key - from Settings → API Keys. +- At least one superuser exists (`manage.py createsuperuser`) with a token + from Settings → Account. ## Known MVP Constraints diff --git a/docs/llm_posting_helper.md b/docs/llm_posting_helper.md index 8fc4194..6cafeb6 100644 --- a/docs/llm_posting_helper.md +++ b/docs/llm_posting_helper.md @@ -24,7 +24,7 @@ DAYBOOK_TOKEN=your-personal-api-token DAYBOOK_SOURCE=agent ``` -`DAYBOOK_TOKEN` is a personal API key, created under Settings → API Keys in the app (one per agent, individually revocable) — not a key shared across users. Override these per environment. +`DAYBOOK_TOKEN` is a personal per-user token, from Settings → Account in the app — not a token shared across users. Override these per environment. ## Commands diff --git a/skills/daybook-posting/SKILL.md b/skills/daybook-posting/SKILL.md index 12dd9ff..c972af9 100644 --- a/skills/daybook-posting/SKILL.md +++ b/skills/daybook-posting/SKILL.md @@ -18,7 +18,7 @@ support; a `.env` file in the working directory is not read automatically. ```text DAYBOOK_URL=https://your-daybook-instance.example.com # your server's base URL -DAYBOOK_TOKEN=your-personal-api-key # from Settings -> API Keys +DAYBOOK_TOKEN=your-personal-api-token # from Settings -> Account DAYBOOK_SOURCE=agent # a stable label for what's posting ``` @@ -27,9 +27,9 @@ DAYBOOK_SOURCE=agent # a stable label for wha `http://127.0.0.1:8001`, which is only correct when Daybook is running locally on the same machine (its development default). For any remote or deployed instance you **must** set this. -- **`DAYBOOK_TOKEN`** — a personal API key, created in the app under Settings → - API Keys (name one per agent so you can revoke them individually). Not a key - shared by everyone; artifacts posted with it land only in your feed. +- **`DAYBOOK_TOKEN`** — your personal per-user API token, from Settings → + Account in the app (not a token shared by everyone). Artifacts posted with + your token land only in your feed. - **`DAYBOOK_SOURCE`** — a free-text label for whatever is posting (agent-profile, script, or person name). Optional; can also be passed per call with `--source`. diff --git a/static/daybook.css b/static/daybook.css index 203eacf..37002a3 100644 --- a/static/daybook.css +++ b/static/daybook.css @@ -990,64 +990,6 @@ kbd { word-break: break-all; } -.muted { - color: var(--muted); -} - -.api-key-create { - align-items: end; - display: flex; - flex-wrap: wrap; - gap: 0.75rem; - margin-bottom: 1.5rem; -} - -.api-key-create label { - display: flex; - flex-direction: column; - font-weight: 600; - gap: 0.3rem; -} - -.api-key-table { - border-collapse: collapse; - width: 100%; -} - -.api-key-table th, -.api-key-table td { - border-bottom: 1px solid var(--accent-border); - padding: 0.55rem 0.7rem; - text-align: left; - vertical-align: middle; -} - -.api-key-table th { - color: var(--muted); - font-size: 0.85em; - font-weight: 600; -} - -.api-key-cell { - align-items: center; - display: flex; - gap: 0.5rem; -} - -.api-key-cell .token-value { - font-size: 0.85em; -} - -.api-key-copy { - flex-shrink: 0; -} - -.api-key-revoke { - background: var(--danger-bg); - border: 1px solid var(--danger-text); - color: var(--danger-text); -} - .about-head { align-items: center; display: flex; diff --git a/templates/artifacts/settings_account.html b/templates/artifacts/settings_account.html index f8329b8..c5cec85 100644 --- a/templates/artifacts/settings_account.html +++ b/templates/artifacts/settings_account.html @@ -4,10 +4,12 @@

Account

Username: {{ request.user.username }}

-

API Keys

-

- Posting to Daybook uses named API keys — one per agent or script, each - revocable on its own. You have {{ key_count }} key{{ key_count|pluralize }}. -

-

Manage API keys →

+

API Token

+

Use this token as Authorization: Bearer <token> when posting artifacts.

+

{{ token.key }}

+
+ {% csrf_token %} + + +
{% endblock %} diff --git a/templates/artifacts/settings_api_keys.html b/templates/artifacts/settings_api_keys.html deleted file mode 100644 index 99dd069..0000000 --- a/templates/artifacts/settings_api_keys.html +++ /dev/null @@ -1,66 +0,0 @@ -{% extends "artifacts/settings_base.html" %} - -{% block settings_content %} -

API Keys

-

- One key per agent or script. Name each key so you know which agent it's - for, copy it into that agent's DAYBOOK_TOKEN, and revoke keys - individually here. Use a key as - Authorization: Bearer <key> when posting. -

- -

Create a key

-
- {% csrf_token %} - - - -
- -

Your keys

- {% if api_keys %} - - - - - - {% for k in api_keys %} - - - - - - - - {% endfor %} - -
NameKeyCreatedLast used
{{ k.name }} - {{ k.key }} - - {{ k.created_at|date:"Y-m-d" }}{% if k.last_used_at %}{{ k.last_used_at|date:"Y-m-d H:i" }}{% else %}never{% endif %} -
- {% csrf_token %} - - - -
-
- {% else %} -

No keys yet. Create one above.

- {% endif %} - - -{% endblock %} diff --git a/templates/artifacts/settings_base.html b/templates/artifacts/settings_base.html index 40b3c01..f88f06f 100644 --- a/templates/artifacts/settings_base.html +++ b/templates/artifacts/settings_base.html @@ -25,7 +25,6 @@