feat: apikey alias/renewal + markdown/patch authorship
- APIKey.alias (unique, required). Creating with an existing alias renews that key: same key string kept, validity reset to 15d, reactivated, name/roles updated (response has renewed=true). - get_actor(): X-API-Key -> key alias, Bearer -> 'admin'. - markdown & patch create/update record author / created_at / updated_at / last_modified_by from the actor. - Idempotent run_migrations() (information_schema-guarded ALTERs + backfill) so existing tables/data gain the new columns on startup; create_all still covers fresh DBs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
from datetime import datetime, timedelta, UTC
|
||||
from flask import Blueprint, request, jsonify
|
||||
from api import generate_api_key
|
||||
from db import get_db
|
||||
@@ -10,13 +11,31 @@ api_key_bp = Blueprint('apikey', __name__, url_prefix='/api/apikey')
|
||||
# product defines, regardless of what the request body asks for.
|
||||
ALLOWED_API_KEY_ROLES = {'admin', 'creator', 'user'}
|
||||
|
||||
# Validity window applied on create and on every renewal.
|
||||
KEY_TTL = timedelta(days=15)
|
||||
|
||||
@api_key_bp.route('/', methods=['POST'])
|
||||
@require_auth(roles=['admin'])
|
||||
def create_key():
|
||||
data = request.get_json(silent=True)
|
||||
"""Create an API key, or renew an existing one.
|
||||
|
||||
if not data or 'name' not in data:
|
||||
`alias` is required and unique. Creating with an alias that already
|
||||
exists is treated as a RENEWAL of that key: the same key string is
|
||||
kept (so existing integrations keep working), its validity window is
|
||||
reset, it is reactivated, and name/roles are updated. The (unchanged)
|
||||
key string is returned again.
|
||||
"""
|
||||
data = request.get_json(silent=True)
|
||||
if not data:
|
||||
return jsonify({"error": "invalid or missing JSON body"}), 400
|
||||
|
||||
alias = data.get('alias')
|
||||
name = data.get('name')
|
||||
if not alias or not str(alias).strip():
|
||||
return jsonify({"error": "alias is required"}), 400
|
||||
if not name:
|
||||
return jsonify({"error": "Name is required"}), 400
|
||||
alias = str(alias).strip()
|
||||
|
||||
roles = data.get('roles', [])
|
||||
if not isinstance(roles, list) or any(r not in ALLOWED_API_KEY_ROLES for r in roles):
|
||||
@@ -24,10 +43,29 @@ def create_key():
|
||||
|
||||
try:
|
||||
with get_db() as session:
|
||||
apikey = APIKey(key=generate_api_key(), name=data['name'], roles=roles)
|
||||
existing = session.query(APIKey).filter_by(alias=alias).first()
|
||||
if existing is not None:
|
||||
# Renewal: keep the key string, reset validity, reactivate.
|
||||
existing.name = name
|
||||
existing.roles = roles
|
||||
existing.is_active = True
|
||||
existing.expire = datetime.now(UTC) + KEY_TTL
|
||||
session.commit()
|
||||
result = existing.to_dict()
|
||||
result['renewed'] = True
|
||||
return jsonify(result), 200
|
||||
|
||||
apikey = APIKey(
|
||||
key=generate_api_key(),
|
||||
alias=alias,
|
||||
name=name,
|
||||
roles=roles,
|
||||
expire=datetime.now(UTC) + KEY_TTL,
|
||||
)
|
||||
session.add(apikey)
|
||||
session.commit()
|
||||
result = apikey.to_dict()
|
||||
result['renewed'] = False
|
||||
return jsonify(result), 201
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
Reference in New Issue
Block a user