add admin api management
This commit is contained in:
@@ -3,9 +3,9 @@ from __future__ import annotations
|
|||||||
from fastapi import HTTPException, Request, status
|
from fastapi import HTTPException, Request, status
|
||||||
from sqladmin import Admin, ModelView
|
from sqladmin import Admin, ModelView
|
||||||
from sqladmin.authentication import AuthenticationBackend
|
from sqladmin.authentication import AuthenticationBackend
|
||||||
from starlette.responses import RedirectResponse
|
from starlette.responses import RedirectResponse, JSONResponse
|
||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker
|
||||||
from wtforms import StringField
|
from wtforms import StringField, TextAreaField
|
||||||
from wtforms.validators import Optional
|
from wtforms.validators import Optional
|
||||||
|
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
@@ -39,27 +39,60 @@ class ApiClientAdmin(ModelView, model=ApiClient):
|
|||||||
|
|
||||||
class ApiKeyAdmin(ModelView, model=ApiKey):
|
class ApiKeyAdmin(ModelView, model=ApiKey):
|
||||||
form_excluded_columns = [ApiKey.key_hash, ApiKey.key_prefix, ApiKey.created_at]
|
form_excluded_columns = [ApiKey.key_hash, ApiKey.key_prefix, ApiKey.created_at]
|
||||||
|
column_list = [ApiKey.id, ApiKey.client_id, ApiKey.name, ApiKey.key_prefix, ApiKey.is_active, ApiKey.created_at]
|
||||||
|
column_details_list = [ApiKey.id, ApiKey.client_id, ApiKey.name, ApiKey.key_prefix, ApiKey.permissions, ApiKey.is_active, ApiKey.created_at]
|
||||||
|
details_template = "apikey_details.html"
|
||||||
|
|
||||||
form_extra_fields = {
|
form_extra_fields = {
|
||||||
"plain_key": StringField("Plain Key", validators=[Optional()]),
|
"permissions": TextAreaField(
|
||||||
"permissions_csv": StringField("Permissions (comma)", validators=[Optional()]),
|
"Permissions (JSON Array)",
|
||||||
|
validators=[Optional()],
|
||||||
|
description='Example: ["feed.waiting-time:write", "feed.opd-checkpoint:write"]',
|
||||||
|
render_kw={"placeholder": '["feed.waiting-time:write"]', "rows": 3}
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
async def on_model_change(self, data: dict, model: ApiKey, is_created: bool, request: Request) -> None:
|
async def on_model_change(self, data: dict, model: ApiKey, is_created: bool, request: Request) -> None:
|
||||||
plain_key = data.get("plain_key")
|
import json
|
||||||
if plain_key:
|
|
||||||
|
# Handle permissions from textarea (JSON format)
|
||||||
|
permissions_str = data.get("permissions")
|
||||||
|
if permissions_str:
|
||||||
|
try:
|
||||||
|
perms = json.loads(permissions_str)
|
||||||
|
if isinstance(perms, list):
|
||||||
|
model.permissions = perms
|
||||||
|
else:
|
||||||
|
raise ValueError("Permissions must be a JSON array")
|
||||||
|
except (json.JSONDecodeError, ValueError) as e:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail=f"Invalid permissions format: {str(e)}. Use JSON array like: [\"feed.waiting-time:write\"]"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Auto-generate key for new records if not provided
|
||||||
|
if is_created and not model.key_hash:
|
||||||
|
plain_key = generate_api_key()
|
||||||
model.key_prefix = get_prefix(plain_key)
|
model.key_prefix = get_prefix(plain_key)
|
||||||
model.key_hash = hash_api_key(plain_key)
|
model.key_hash = hash_api_key(plain_key)
|
||||||
|
# Store in session for display after creation
|
||||||
permissions_csv = data.get("permissions_csv")
|
request.session[f"new_api_key_{model.client_id}"] = plain_key
|
||||||
if permissions_csv is not None:
|
|
||||||
perms = [p.strip() for p in permissions_csv.split(",") if p.strip()]
|
|
||||||
model.permissions = perms
|
|
||||||
|
|
||||||
|
|
||||||
def mount_admin(app):
|
def mount_admin(app):
|
||||||
|
import os
|
||||||
|
|
||||||
auth_backend = AdminAuth(secret_key=settings.ADMIN_SECRET_KEY)
|
auth_backend = AdminAuth(secret_key=settings.ADMIN_SECRET_KEY)
|
||||||
admin = Admin(app=app, engine=engine, authentication_backend=auth_backend)
|
|
||||||
|
# Configure templates directory
|
||||||
|
templates_dir = os.path.join(os.path.dirname(__file__), "templates")
|
||||||
|
|
||||||
|
admin = Admin(
|
||||||
|
app=app,
|
||||||
|
engine=engine,
|
||||||
|
authentication_backend=auth_backend,
|
||||||
|
templates_dir=templates_dir
|
||||||
|
)
|
||||||
|
|
||||||
SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False)
|
SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False)
|
||||||
|
|
||||||
@@ -105,3 +138,67 @@ def mount_admin(app):
|
|||||||
return {"key_id": api_key.id, "api_key": plain_key, "permissions": perms}
|
return {"key_id": api_key.id, "api_key": plain_key, "permissions": perms}
|
||||||
finally:
|
finally:
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
|
@app.post("/admin/api-keys/{key_id}/regenerate")
|
||||||
|
async def _admin_regenerate_api_key(request: Request, key_id: int):
|
||||||
|
"""Regenerate API key - creates new key while preserving permissions and other settings."""
|
||||||
|
if not request.session.get("admin"):
|
||||||
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Not authenticated")
|
||||||
|
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
api_key = db.get(ApiKey, key_id)
|
||||||
|
if not api_key:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="API Key not found")
|
||||||
|
|
||||||
|
# Generate new key
|
||||||
|
plain_key = generate_api_key()
|
||||||
|
api_key.key_prefix = get_prefix(plain_key)
|
||||||
|
api_key.key_hash = hash_api_key(plain_key)
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
db.refresh(api_key)
|
||||||
|
|
||||||
|
return JSONResponse({
|
||||||
|
"success": True,
|
||||||
|
"key_id": api_key.id,
|
||||||
|
"api_key": plain_key,
|
||||||
|
"key_prefix": api_key.key_prefix,
|
||||||
|
"permissions": api_key.permissions,
|
||||||
|
"message": "API key regenerated successfully. Please save this key - it won't be shown again!"
|
||||||
|
})
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
@app.get("/admin/api-keys/{key_id}/view")
|
||||||
|
async def _admin_view_api_key(request: Request, key_id: int):
|
||||||
|
"""View API key from session (only works for newly created/regenerated keys)."""
|
||||||
|
if not request.session.get("admin"):
|
||||||
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Not authenticated")
|
||||||
|
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
api_key = db.get(ApiKey, key_id)
|
||||||
|
if not api_key:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="API Key not found")
|
||||||
|
|
||||||
|
# Check if there's a stored key in session
|
||||||
|
session_key = f"new_api_key_{api_key.client_id}"
|
||||||
|
plain_key = request.session.get(session_key)
|
||||||
|
|
||||||
|
if plain_key:
|
||||||
|
# Clear from session after viewing
|
||||||
|
request.session.pop(session_key, None)
|
||||||
|
return JSONResponse({
|
||||||
|
"success": True,
|
||||||
|
"api_key": plain_key,
|
||||||
|
"key_prefix": api_key.key_prefix,
|
||||||
|
"message": "This is the only time you can view this key!"
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
return JSONResponse({
|
||||||
|
"success": False,
|
||||||
|
"message": "API key cannot be retrieved. Keys can only be viewed once after creation or regeneration."
|
||||||
|
}, status_code=400)
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|||||||
96
03-apiservice/app/templates/apikey_details.html
Normal file
96
03-apiservice/app/templates/apikey_details.html
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
{% extends "sqladmin/details.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{{ super() }}
|
||||||
|
|
||||||
|
<div class="card mt-3">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5>API Key Actions</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<button type="button" class="btn btn-warning me-2" onclick="regenerateApiKey({{ model.id }})">
|
||||||
|
<i class="fa fa-refresh"></i> Regenerate API Key
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-info" onclick="viewApiKey({{ model.id }})">
|
||||||
|
<i class="fa fa-eye"></i> View API Key (if just created)
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div id="apiKeyResult" class="mt-3" style="display: none;">
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<h6>⚠️ Important: Save this key immediately!</h6>
|
||||||
|
<p>This key will only be shown once and cannot be retrieved later.</p>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" id="apiKeyValue" class="form-control" readonly>
|
||||||
|
<button class="btn btn-primary" onclick="copyApiKey()">
|
||||||
|
<i class="fa fa-copy"></i> Copy
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<small id="apiKeyMessage" class="text-muted mt-2 d-block"></small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
async function regenerateApiKey(keyId) {
|
||||||
|
if (!confirm('Are you sure you want to regenerate this API key? The old key will stop working immediately!')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/apiservice/admin/api-keys/${keyId}/regenerate`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
document.getElementById('apiKeyValue').value = data.api_key;
|
||||||
|
document.getElementById('apiKeyMessage').textContent = data.message;
|
||||||
|
document.getElementById('apiKeyResult').style.display = 'block';
|
||||||
|
|
||||||
|
alert('✅ API Key regenerated successfully!\n\nNew Key: ' + data.api_key + '\n\nPlease copy and save it now!');
|
||||||
|
} else {
|
||||||
|
alert('❌ Error: ' + (data.message || 'Failed to regenerate API key'));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
alert('❌ Error: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function viewApiKey(keyId) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/apiservice/admin/api-keys/${keyId}/view`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
document.getElementById('apiKeyValue').value = data.api_key;
|
||||||
|
document.getElementById('apiKeyMessage').textContent = data.message;
|
||||||
|
document.getElementById('apiKeyResult').style.display = 'block';
|
||||||
|
|
||||||
|
alert('✅ API Key retrieved!\n\nKey: ' + data.api_key + '\n\nPlease copy and save it now!');
|
||||||
|
} else {
|
||||||
|
alert('ℹ️ ' + data.message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
alert('❌ Error: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyApiKey() {
|
||||||
|
const input = document.getElementById('apiKeyValue');
|
||||||
|
input.select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
alert('✅ API Key copied to clipboard!');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user