426 lines
14 KiB
Python
Raw Normal View History

import logging
from typing import Annotated
from fastapi import APIRouter, Request, Form
from devplacepy.models import AdminRoleForm, AdminPasswordForm, AdminSettingsForm
from fastapi.responses import HTMLResponse, JSONResponse
from devplacepy.database import (
get_table,
build_pagination,
get_post_counts_by_user_uids,
get_news_images_by_uids,
clear_settings_cache,
get_platform_analytics,
get_deleted_media,
)
from devplacepy.attachments import delete_attachment
from devplacepy.utils import (
require_admin,
hash_password,
generate_uid,
time_ago,
clear_user_cache,
get_current_user,
is_admin,
)
from devplacepy.seo import base_seo_context, site_url, website_schema
from devplacepy.responses import respond, action_result
from devplacepy.schemas import (
AdminUsersOut,
AdminNewsOut,
AdminSettingsOut,
AdminMediaOut,
GatewayUsageOut,
)
from devplacepy.services.manager import service_manager
from devplacepy.services.openai_gateway.analytics import (
build_analytics,
build_user_usage,
)
from devplacepy.services.openai_gateway.usage import pricing_from_cfg
logger = logging.getLogger(__name__)
router = APIRouter()
@router.get("", response_class=HTMLResponse)
async def admin_index(request: Request):
require_admin(request)
return action_result(request, "/admin/users")
@router.get("/analytics")
async def admin_analytics(request: Request, top_n: int = 10):
user = get_current_user(request)
if not user:
return JSONResponse({"error": "Authentication required"}, status_code=401)
if not is_admin(user):
return JSONResponse({"error": "Admin access required"}, status_code=403)
return JSONResponse(get_platform_analytics(top_n))
@router.get("/ai-usage", response_class=HTMLResponse)
async def admin_ai_usage(request: Request):
admin = require_admin(request)
base = site_url(request)
seo_ctx = base_seo_context(
request,
title="AI usage - Admin",
description="AI gateway token usage, cost, latency, and reliability metrics.",
breadcrumbs=[
{"name": "Home", "url": "/feed"},
{"name": "Admin", "url": "/admin"},
{"name": "AI usage", "url": "/admin/ai-usage"},
],
schemas=[website_schema(base)],
)
return respond(
request,
"ai_usage.html",
{
**seo_ctx,
"request": request,
"user": admin,
"admin_section": "ai-usage",
},
model=GatewayUsageOut,
)
@router.get("/ai-usage/data")
async def admin_ai_usage_data(request: Request, hours: int = 48, top_n: int = 10):
user = get_current_user(request)
if not user:
return JSONResponse({"error": "Authentication required"}, status_code=401)
if not is_admin(user):
return JSONResponse({"error": "Admin access required"}, status_code=403)
service = service_manager.get_service("openai")
pricing = pricing_from_cfg(service.get_config()) if service is not None else None
return JSONResponse(build_analytics(hours, top_n=top_n, pricing=pricing))
@router.get("/users/{uid}/ai-usage")
async def admin_user_ai_usage(request: Request, uid: str, hours: int = 24):
user = get_current_user(request)
if not user:
return JSONResponse({"error": "Authentication required"}, status_code=401)
if not is_admin(user):
return JSONResponse({"error": "Admin access required"}, status_code=403)
service = service_manager.get_service("openai")
pricing = pricing_from_cfg(service.get_config()) if service is not None else None
return JSONResponse(build_user_usage(uid, hours=hours, pricing=pricing))
@router.get("/users", response_class=HTMLResponse)
async def admin_users(request: Request, page: int = 1):
admin = require_admin(request)
users_table = get_table("users")
total = users_table.count()
pagination = build_pagination(page, total)
offset = (pagination["page"] - 1) * pagination["per_page"]
page_users = list(
users_table.find(
order_by=["-created_at"], _limit=pagination["per_page"], _offset=offset
)
)
post_counts = get_post_counts_by_user_uids([u["uid"] for u in page_users])
for u in page_users:
u["posts_count"] = post_counts.get(u["uid"], 0)
base = site_url(request)
seo_ctx = base_seo_context(
request,
title="Users - Admin",
description="Manage DevPlace users.",
breadcrumbs=[
{"name": "Home", "url": "/feed"},
{"name": "Admin", "url": "/admin"},
{"name": "Users", "url": "/admin/users"},
],
schemas=[website_schema(base)],
)
return respond(
request,
"admin_users.html",
{
**seo_ctx,
"request": request,
"user": admin,
"users": page_users,
"pagination": pagination,
"admin_section": "users",
},
model=AdminUsersOut,
)
@router.post("/users/{uid}/role")
async def admin_user_role(
request: Request, uid: str, data: Annotated[AdminRoleForm, Form()]
):
admin = require_admin(request)
role = data.role.capitalize()
if uid == admin["uid"]:
return action_result(request, "/admin/users")
users = get_table("users")
users.update({"uid": uid, "role": role}, ["uid"])
clear_user_cache(uid)
logger.info(f"Admin {admin['username']} set user {uid} role to {role}")
return action_result(request, "/admin/users")
@router.post("/users/{uid}/password")
async def admin_user_password(
request: Request, uid: str, data: Annotated[AdminPasswordForm, Form()]
):
admin = require_admin(request)
users = get_table("users")
users.update({"uid": uid, "password_hash": hash_password(data.password)}, ["uid"])
logger.info(f"Admin {admin['username']} changed password for user {uid}")
return action_result(request, "/admin/users")
@router.post("/users/{uid}/toggle")
async def admin_user_toggle(request: Request, uid: str):
admin = require_admin(request)
if uid == admin["uid"]:
return action_result(request, "/admin/users")
users = get_table("users")
user = users.find_one(uid=uid)
if user:
new_state = not user.get("is_active", True)
users.update({"uid": uid, "is_active": new_state}, ["uid"])
clear_user_cache(uid)
logger.info(
f"Admin {admin['username']} {'disabled' if not new_state else 'enabled'} user {uid}"
)
return action_result(request, "/admin/users")
@router.post("/users/{uid}/reset-ai-quota")
async def admin_user_reset_ai_quota(request: Request, uid: str):
admin = require_admin(request)
devii = service_manager.get_service("devii")
removed = devii.reset_quota("user", uid) if devii is not None else 0
logger.info(
f"Admin {admin['username']} reset AI quota for user {uid} ({removed} ledger rows)"
)
return action_result(request, "/admin/users")
@router.get("/media", response_class=HTMLResponse)
async def admin_media(request: Request, page: int = 1):
admin = require_admin(request)
media, pagination = get_deleted_media(page)
base = site_url(request)
seo_ctx = base_seo_context(
request,
title="Media - Admin",
description="Restore or permanently remove soft-deleted media.",
breadcrumbs=[
{"name": "Home", "url": "/feed"},
{"name": "Admin", "url": "/admin"},
{"name": "Media", "url": "/admin/media"},
],
schemas=[website_schema(base)],
)
return respond(
request,
"admin_media.html",
{
**seo_ctx,
"request": request,
"user": admin,
"media": media,
"pagination": pagination,
"admin_section": "media",
},
model=AdminMediaOut,
)
@router.post("/media/{uid}/purge")
async def admin_media_purge(request: Request, uid: str):
admin = require_admin(request)
delete_attachment(uid)
logger.info(f"Admin {admin['username']} purged media {uid}")
return action_result(request, "/admin/media")
@router.post("/ai-quota/reset-guests")
async def admin_reset_guest_ai_quota(request: Request):
admin = require_admin(request)
devii = service_manager.get_service("devii")
removed = devii.reset_guest_quotas() if devii is not None else 0
logger.info(
f"Admin {admin['username']} reset all guest AI quotas ({removed} ledger rows)"
)
return action_result(request, "/admin/ai-usage")
@router.post("/ai-quota/reset-all")
async def admin_reset_all_ai_quota(request: Request):
admin = require_admin(request)
devii = service_manager.get_service("devii")
removed = devii.reset_all_quotas() if devii is not None else 0
logger.info(
f"Admin {admin['username']} reset ALL AI quotas ({removed} ledger rows)"
)
return action_result(request, "/admin/ai-usage")
@router.get("/settings", response_class=HTMLResponse)
async def admin_settings(request: Request):
admin = require_admin(request)
settings = get_table("site_settings")
all_settings = {s["key"]: s["value"] for s in settings.all()}
base = site_url(request)
seo_ctx = base_seo_context(
request,
title="Settings - Admin",
description="Manage DevPlace site settings.",
breadcrumbs=[
{"name": "Home", "url": "/feed"},
{"name": "Admin", "url": "/admin"},
{"name": "Settings", "url": "/admin/settings"},
],
schemas=[website_schema(base)],
)
return respond(
request,
"admin_settings.html",
{
**seo_ctx,
"request": request,
"user": admin,
"settings": all_settings,
"admin_section": "settings",
},
model=AdminSettingsOut,
)
@router.get("/news", response_class=HTMLResponse)
async def admin_news(request: Request, page: int = 1):
admin = require_admin(request)
news_table = get_table("news")
total = news_table.count()
pagination = build_pagination(page, total)
offset = (pagination["page"] - 1) * pagination["per_page"]
page_articles = list(
news_table.find(
order_by=["-synced_at"], _limit=pagination["per_page"], _offset=offset
)
)
images_by_news = get_news_images_by_uids([a["uid"] for a in page_articles])
enriched = []
for a in page_articles:
enriched.append(
{
"article": a,
"time_ago": time_ago(a["synced_at"]),
"synced_at": a.get("synced_at", ""),
"grade": a.get("grade", 0),
"has_image": a["uid"] in images_by_news,
}
)
base = site_url(request)
seo_ctx = base_seo_context(
request,
title="News - Admin",
description="Manage DevPlace news articles.",
breadcrumbs=[
{"name": "Home", "url": "/feed"},
{"name": "Admin", "url": "/admin"},
{"name": "News", "url": "/admin/news"},
],
schemas=[website_schema(base)],
)
return respond(
request,
"admin_news.html",
{
**seo_ctx,
"request": request,
"user": admin,
"articles": enriched,
"pagination": pagination,
"admin_section": "news",
},
model=AdminNewsOut,
)
@router.post("/news/{uid}/toggle")
async def admin_news_toggle(request: Request, uid: str):
admin = require_admin(request)
news_table = get_table("news")
article = news_table.find_one(uid=uid)
if article:
current = article.get("featured", 0)
news_table.update({"uid": uid, "featured": 0 if current else 1}, ["uid"])
logger.info(
f"Admin {admin['username']} toggled news {uid} featured={'off' if current else 'on'}"
)
return action_result(request, "/admin/news")
@router.post("/news/{uid}/publish")
async def admin_news_publish(request: Request, uid: str):
admin = require_admin(request)
news_table = get_table("news")
article = news_table.find_one(uid=uid)
if article:
current = article.get("status", "draft")
new_status = "draft" if current == "published" else "published"
news_table.update({"uid": uid, "status": new_status}, ["uid"])
logger.info(f"Admin {admin['username']} set news {uid} status={new_status}")
return action_result(request, "/admin/news")
@router.post("/news/{uid}/landing")
async def admin_news_landing(request: Request, uid: str):
admin = require_admin(request)
news_table = get_table("news")
article = news_table.find_one(uid=uid)
if article:
current = article.get("show_on_landing", 0)
news_table.update({"uid": uid, "show_on_landing": 0 if current else 1}, ["uid"])
logger.info(
f"Admin {admin['username']} toggled news {uid} landing={'on' if not current else 'off'}"
)
return action_result(request, "/admin/news")
@router.post("/news/{uid}/delete")
async def admin_news_delete(request: Request, uid: str):
admin = require_admin(request)
news_table = get_table("news")
article = news_table.find_one(uid=uid)
if article:
images_table = get_table("news_images")
for img in images_table.find(news_uid=uid):
images_table.delete(id=img["id"])
news_table.delete(id=article["id"])
logger.info(f"Admin {admin['username']} deleted news {uid}")
return action_result(request, "/admin/news")
@router.post("/settings")
async def admin_settings_save(
request: Request, data: Annotated[AdminSettingsForm, Form()]
):
admin = require_admin(request)
settings = get_table("site_settings")
for key, value in data.model_dump().items():
existing = settings.find_one(key=key)
if existing:
if value == "":
continue
settings.update({"id": existing["id"], "key": key, "value": value}, ["id"])
else:
settings.insert({"uid": generate_uid(), "key": key, "value": value})
clear_settings_cache()
logger.info(f"Admin {admin['username']} updated settings")
return action_result(request, "/admin/settings")