diff --git a/routes/api_token_routes.py b/routes/api_token_routes.py
index cbc828731..6072e43fd 100644
--- a/routes/api_token_routes.py
+++ b/routes/api_token_routes.py
@@ -8,7 +8,7 @@
from core.database import get_db_session, ApiToken
from core.middleware import require_admin
-from src.auth_helpers import get_current_user
+from src.auth_helpers import get_current_user, require_user
MAX_NAME_LEN = 100
DEFAULT_SCOPES = "chat"
@@ -206,4 +206,82 @@ def delete_token(request: Request, token_id: str):
_invalidate_cache(request)
return {"status": "deleted"}
+ # ── Self-serve endpoints (cookie-only, owner forced to current user) ──
+
+ @router.get("/tokens/self")
+ def list_self_tokens(request: Request):
+ """List the current user's own API tokens. Cookie session only."""
+ user = require_user(request)
+ with get_db_session() as db:
+ tokens = db.query(ApiToken).filter(
+ ApiToken.owner == user, ApiToken.is_active == True # noqa: E712
+ ).all()
+ return [
+ {
+ "id": t.id,
+ "name": t.name,
+ "owner": getattr(t, "owner", None),
+ "token_prefix": t.token_prefix,
+ "scopes": [s.strip() for s in (getattr(t, "scopes", "") or DEFAULT_SCOPES).split(",") if s.strip()],
+ "is_active": t.is_active,
+ "last_used_at": t.last_used_at.isoformat() if t.last_used_at else None,
+ "created_at": t.created_at.isoformat() if t.created_at else None,
+ }
+ for t in tokens
+ ]
+
+ @router.post("/tokens/self")
+ def create_self_token(
+ request: Request,
+ name: str = Form(""),
+ scopes: str = Form(None),
+ profile: str = Form(None),
+ ):
+ """Create an API token for the current user. Cookie session only."""
+ user = require_user(request)
+ name = name.strip()[:MAX_NAME_LEN]
+ if not name:
+ raise HTTPException(400, "Token name is required")
+ scope_list = _normalize_scopes(scopes, profile)
+ scopes_value = ",".join(scope_list)
+
+ raw_token = "ody_" + secrets.token_urlsafe(32)
+ token_hash = bcrypt.hashpw(raw_token.encode(), bcrypt.gensalt()).decode()
+ token_id = str(uuid.uuid4())[:8]
+
+ with get_db_session() as db:
+ db.add(ApiToken(
+ id=token_id,
+ owner=user,
+ name=name,
+ token_hash=token_hash,
+ token_prefix=raw_token[:8],
+ scopes=scopes_value,
+ is_active=True,
+ ))
+ _invalidate_cache(request)
+
+ return {
+ "id": token_id,
+ "name": name,
+ "owner": user,
+ "token": raw_token,
+ "token_prefix": raw_token[:8],
+ "scopes": scope_list,
+ }
+
+ @router.delete("/tokens/self/{token_id}")
+ def delete_self_token(request: Request, token_id: str):
+ """Delete one of the current user's own API tokens. Cookie session only."""
+ user = require_user(request)
+ with get_db_session() as db:
+ token = db.query(ApiToken).filter(ApiToken.id == token_id).first()
+ # Return 404 for both "not found" and "not yours" so callers
+ # can't probe for the existence of another user's token ids.
+ if not token or token.owner != user:
+ raise HTTPException(404, "Token not found")
+ db.delete(token)
+ _invalidate_cache(request)
+ return {"status": "deleted"}
+
return router
diff --git a/static/index.html b/static/index.html
index 8257660fe..cf77499f9 100644
--- a/static/index.html
+++ b/static/index.html
@@ -1999,6 +1999,34 @@