admin nice

This commit is contained in:
root 2025-10-05 19:07:26 +00:00
parent c170ca5433
commit 721af9bc83
2 changed files with 883 additions and 1 deletions

View File

@ -36,16 +36,19 @@ from app.api.routes.admin import (
s_api_v1_admin_blockchain,
s_api_v1_admin_cache_cleanup,
s_api_v1_admin_cache_setlimits,
s_api_v1_admin_licenses,
s_api_v1_admin_login,
s_api_v1_admin_logout,
s_api_v1_admin_node_setrole,
s_api_v1_admin_nodes,
s_api_v1_admin_overview,
s_api_v1_admin_stars,
s_api_v1_admin_status,
s_api_v1_admin_storage,
s_api_v1_admin_sync_setlimits,
s_api_v1_admin_system,
s_api_v1_admin_uploads,
s_api_v1_admin_users,
)
from app.api.routes.tonconnect import s_api_v1_tonconnect_new, s_api_v1_tonconnect_logout
from app.api.routes.keys import s_api_v1_keys_request
@ -98,6 +101,9 @@ app.add_route(s_api_v1_admin_logout, "/api/v1/admin.logout", methods=["POST", "O
app.add_route(s_api_v1_admin_overview, "/api/v1/admin.overview", methods=["GET", "OPTIONS"])
app.add_route(s_api_v1_admin_storage, "/api/v1/admin.storage", methods=["GET", "OPTIONS"])
app.add_route(s_api_v1_admin_uploads, "/api/v1/admin.uploads", methods=["GET", "OPTIONS"])
app.add_route(s_api_v1_admin_users, "/api/v1/admin.users", methods=["GET", "OPTIONS"])
app.add_route(s_api_v1_admin_licenses, "/api/v1/admin.licenses", methods=["GET", "OPTIONS"])
app.add_route(s_api_v1_admin_stars, "/api/v1/admin.stars", methods=["GET", "OPTIONS"])
app.add_route(s_api_v1_admin_system, "/api/v1/admin.system", methods=["GET", "OPTIONS"])
app.add_route(s_api_v1_admin_blockchain, "/api/v1/admin.blockchain", methods=["GET", "OPTIONS"])
app.add_route(s_api_v1_admin_node_setrole, "/api/v1/admin.node.setRole", methods=["POST", "OPTIONS"])

View File

@ -11,7 +11,7 @@ from typing import Any, Dict, List, Optional
from base58 import b58encode
from sanic import response
from sqlalchemy import func, select
from sqlalchemy import Integer, String, and_, case, cast, func, or_, select
from app.api.routes._system import get_git_info
from app.core._blockchain.ton.platform import platform
@ -39,6 +39,9 @@ from app.core.models.tasks import BlockchainTask
from app.core.models.node_storage import StoredContent
from app.core.models.user import User
from app.core.models.content.user_content import UserContent
from app.core.models.transaction import StarsInvoice
from app.core.models.wallet_connection import WalletConnection
from app.core.models.user_activity import UserActivity
from app.core._utils.share_links import build_content_links
from app.core.content.content_id import ContentId
@ -194,6 +197,17 @@ def _pick_primary_download(candidates: List[tuple[str, Optional[str], Optional[i
return None
def _parse_bool_arg(value: Optional[str]) -> Optional[bool]:
if value is None:
return None
normalized = value.strip().lower()
if normalized in {'1', 'true', 'yes', 'y', 'on'}:
return True
if normalized in {'0', 'false', 'no', 'n', 'off'}:
return False
return None
async def s_api_v1_admin_login(request):
token = os.getenv('ADMIN_API_TOKEN')
if not token:
@ -738,6 +752,868 @@ async def s_api_v1_admin_uploads(request):
return response.json(payload)
async def s_api_v1_admin_users(request):
if (unauth := _ensure_admin(request)):
return unauth
session = request.ctx.db_session
try:
limit = int(request.args.get('limit') or 50)
except (TypeError, ValueError):
limit = 50
limit = max(1, min(limit, 200))
try:
offset = int(request.args.get('offset') or 0)
except (TypeError, ValueError):
offset = 0
offset = max(0, offset)
search = (request.args.get('search') or '').strip()
filters = []
if search:
search_like = f"%{search}%"
clauses = [
cast(User.id, String).ilike(search_like),
cast(User.telegram_id, String).ilike(search_like),
User.username.ilike(search_like),
User.meta['first_name'].astext.ilike(search_like),
User.meta['last_name'].astext.ilike(search_like),
]
numeric_candidate = search.lstrip('-')
if numeric_candidate.isdigit():
try:
search_int = int(search)
clauses.append(User.id == search_int)
clauses.append(User.telegram_id == search_int)
except ValueError:
pass
filters.append(or_(*clauses))
total_stmt = select(func.count()).select_from(User)
if filters:
total_stmt = total_stmt.where(and_(*filters))
total = (await session.execute(total_stmt)).scalar_one()
query_stmt = (
select(User)
.order_by(User.last_use.desc())
.offset(offset)
.limit(limit)
)
if filters:
query_stmt = query_stmt.where(and_(*filters))
user_rows = (await session.execute(query_stmt)).scalars().all()
base_payload = {
'total': int(total or 0),
'limit': limit,
'offset': offset,
'search': search or None,
'has_more': (offset + limit) < int(total or 0),
}
if not user_rows:
base_payload.update({
'items': [],
'summary': {
'users_returned': 0,
'wallets_total': 0,
'wallets_active': 0,
'licenses_total': 0,
'licenses_active': 0,
'stars_total': 0,
'stars_paid': 0,
'stars_unpaid': 0,
'stars_amount_paid': 0,
'stars_amount_unpaid': 0,
'unique_ips_total': 0,
},
})
return response.json(base_payload)
user_ids = [user.id for user in user_rows if user.id is not None]
wallet_map: Dict[int, List[WalletConnection]] = defaultdict(list)
if user_ids:
wallet_rows = (await session.execute(
select(WalletConnection)
.where(WalletConnection.user_id.in_(user_ids))
.order_by(WalletConnection.created.desc())
)).scalars().all()
for wallet in wallet_rows:
if wallet.user_id is not None:
wallet_map[wallet.user_id].append(wallet)
content_stats_map: Dict[int, Dict[str, int]] = {}
if user_ids:
content_rows = (await session.execute(
select(
StoredContent.user_id,
func.count().label('total'),
func.sum(case((StoredContent.type.like('onchain%'), 1), else_=0)).label('onchain'),
func.sum(case((StoredContent.disabled.isnot(None), 1), else_=0)).label('disabled'),
)
.where(StoredContent.user_id.in_(user_ids))
.group_by(StoredContent.user_id)
)).all()
for user_id, total_cnt, onchain_cnt, disabled_cnt in content_rows:
if user_id is None:
continue
total_value = int(total_cnt or 0)
onchain_value = int(onchain_cnt or 0)
disabled_value = int(disabled_cnt or 0)
content_stats_map[user_id] = {
'total': total_value,
'onchain': onchain_value,
'local': max(total_value - onchain_value, 0),
'disabled': disabled_value,
}
license_type_map: Dict[int, Dict[str, int]] = defaultdict(lambda: defaultdict(int))
license_status_map: Dict[int, Dict[str, int]] = defaultdict(lambda: defaultdict(int))
if user_ids:
license_type_rows = (await session.execute(
select(UserContent.user_id, UserContent.type, func.count())
.where(UserContent.user_id.in_(user_ids))
.group_by(UserContent.user_id, UserContent.type)
)).all()
for user_id, ltype, count_value in license_type_rows:
if user_id is None:
continue
license_type_map[user_id][ltype or 'unknown'] += int(count_value or 0)
license_status_rows = (await session.execute(
select(UserContent.user_id, UserContent.status, func.count())
.where(UserContent.user_id.in_(user_ids))
.group_by(UserContent.user_id, UserContent.status)
)).all()
for user_id, status_value, count_value in license_status_rows:
if user_id is None:
continue
license_status_map[user_id][status_value or 'unknown'] += int(count_value or 0)
stars_stats_map: Dict[int, Dict[str, int]] = {}
if user_ids:
stars_rows = (await session.execute(
select(
StarsInvoice.user_id,
func.count().label('total'),
func.sum(case((StarsInvoice.paid.is_(True), 1), else_=0)).label('paid'),
func.sum(case((or_(StarsInvoice.paid.is_(False), StarsInvoice.paid.is_(None)), 1), else_=0)).label('unpaid'),
func.sum(StarsInvoice.amount).label('amount_total'),
func.sum(case((StarsInvoice.paid.is_(True), StarsInvoice.amount), else_=0)).label('amount_paid'),
)
.where(StarsInvoice.user_id.in_(user_ids))
.group_by(StarsInvoice.user_id)
)).all()
for user_id, total_cnt, paid_cnt, unpaid_cnt, amt_total, amt_paid in stars_rows:
if user_id is None:
continue
total_value = int(total_cnt or 0)
paid_value = int(paid_cnt or 0)
unpaid_value = int(unpaid_cnt or max(total_value - paid_value, 0))
amount_total_value = int(amt_total or 0)
amount_paid_value = int(amt_paid or 0)
amount_unpaid_value = max(amount_total_value - amount_paid_value, 0)
stars_stats_map[user_id] = {
'total': total_value,
'paid': paid_value,
'unpaid': unpaid_value,
'amount_total': amount_total_value,
'amount_paid': amount_paid_value,
'amount_unpaid': amount_unpaid_value,
}
ip_counts_map: Dict[int, int] = {}
if user_ids:
ip_count_rows = (await session.execute(
select(
UserActivity.user_id,
func.count(func.distinct(UserActivity.user_ip)),
)
.where(UserActivity.user_id.in_(user_ids), UserActivity.user_ip.isnot(None))
.group_by(UserActivity.user_id)
)).all()
for user_id, ip_count in ip_count_rows:
if user_id is None:
continue
ip_counts_map[user_id] = int(ip_count or 0)
latest_ip_map: Dict[int, Dict[str, Optional[str]]] = {}
if user_ids:
latest_ip_rows = (await session.execute(
select(
UserActivity.user_id,
UserActivity.user_ip,
UserActivity.type,
UserActivity.created,
)
.where(UserActivity.user_id.in_(user_ids), UserActivity.user_ip.isnot(None))
.order_by(UserActivity.user_id, UserActivity.created.desc())
.distinct(UserActivity.user_id)
)).all()
for user_id, ip_value, activity_type, created_at in latest_ip_rows:
if user_id is None:
continue
latest_ip_map[user_id] = {
'ip': ip_value,
'type': activity_type,
'seen_at': _format_dt(created_at),
}
recent_ip_map: Dict[int, List[Dict[str, Optional[str]]]] = defaultdict(list)
if user_ids:
activity_limit = max(200, len(user_ids) * 10)
recent_ip_rows = (await session.execute(
select(
UserActivity.user_id,
UserActivity.user_ip,
UserActivity.type,
UserActivity.created,
)
.where(UserActivity.user_id.in_(user_ids), UserActivity.user_ip.isnot(None))
.order_by(UserActivity.created.desc())
.limit(activity_limit)
)).all()
for user_id, ip_value, activity_type, created_at in recent_ip_rows:
if user_id is None or not ip_value:
continue
bucket = recent_ip_map[user_id]
if len(bucket) >= 5:
continue
bucket.append({
'ip': ip_value,
'type': activity_type,
'seen_at': _format_dt(created_at),
})
items: List[Dict[str, Any]] = []
summary = {
'users_returned': 0,
'wallets_total': 0,
'wallets_active': 0,
'licenses_total': 0,
'licenses_active': 0,
'stars_total': 0,
'stars_paid': 0,
'stars_unpaid': 0,
'stars_amount_total': 0,
'stars_amount_paid': 0,
'stars_amount_unpaid': 0,
'unique_ips_total': 0,
}
for user in user_rows:
summary['users_returned'] += 1
meta = user.meta or {}
wallet_list = wallet_map.get(user.id, [])
active_wallets = [wallet for wallet in wallet_list if not wallet.invalidated]
primary_wallet = active_wallets[0] if active_wallets else None
last_connected_wallet = active_wallets[0] if active_wallets else (wallet_list[0] if wallet_list else None)
wallet_payload = {
'primary_address': primary_wallet.wallet_address if primary_wallet else None,
'active_count': len(active_wallets),
'total_count': len(wallet_list),
'last_connected_at': _format_dt(last_connected_wallet.created) if last_connected_wallet else None,
'connections': [
{
'id': wallet.id,
'address': wallet.wallet_address,
'network': wallet.network,
'invalidated': wallet.invalidated,
'created_at': _format_dt(wallet.created),
'updated_at': _format_dt(wallet.updated),
}
for wallet in wallet_list[:3]
],
}
summary['wallets_total'] += wallet_payload['total_count']
summary['wallets_active'] += wallet_payload['active_count']
content_stats = content_stats_map.get(user.id, {'total': 0, 'onchain': 0, 'local': 0, 'disabled': 0})
license_types = dict(license_type_map.get(user.id, {}))
license_statuses = dict(license_status_map.get(user.id, {}))
licenses_total = sum(license_types.values()) if license_types else sum(license_statuses.values())
licenses_active = license_statuses.get('active', 0)
summary['licenses_total'] += licenses_total
summary['licenses_active'] += licenses_active
stars_stats = stars_stats_map.get(
user.id,
{
'total': 0,
'paid': 0,
'unpaid': 0,
'amount_total': 0,
'amount_paid': 0,
'amount_unpaid': 0,
},
)
summary['stars_total'] += stars_stats['total']
summary['stars_paid'] += stars_stats['paid']
summary['stars_unpaid'] += stars_stats['unpaid']
summary['stars_amount_total'] += stars_stats['amount_total']
summary['stars_amount_paid'] += stars_stats['amount_paid']
summary['stars_amount_unpaid'] += stars_stats['amount_unpaid']
unique_ips = ip_counts_map.get(user.id, 0)
summary['unique_ips_total'] += unique_ips
recent_ips = recent_ip_map.get(user.id, [])
latest_ip = latest_ip_map.get(user.id)
items.append({
'id': user.id,
'telegram_id': user.telegram_id,
'username': user.username,
'first_name': meta.get('first_name'),
'last_name': meta.get('last_name'),
'lang_code': user.lang_code,
'created_at': _format_dt(user.created),
'updated_at': _format_dt(user.updated),
'last_use': _format_dt(user.last_use),
'meta': {
'ref_id': meta.get('ref_id'),
'referrer_id': meta.get('referrer_id'),
},
'wallets': wallet_payload,
'content': content_stats,
'licenses': {
'total': licenses_total,
'active': licenses_active,
'by_type': license_types,
'by_status': license_statuses,
},
'stars': {
'total': stars_stats['total'],
'paid': stars_stats['paid'],
'unpaid': stars_stats['unpaid'],
'amount_total': stars_stats['amount_total'],
'amount_paid': stars_stats['amount_paid'],
'amount_unpaid': stars_stats['amount_unpaid'],
},
'ip_activity': {
'last': latest_ip or None,
'unique_ips': unique_ips,
'recent': recent_ips,
},
})
base_payload.update({
'items': items,
'summary': summary,
})
return response.json(base_payload)
async def s_api_v1_admin_licenses(request):
if (unauth := _ensure_admin(request)):
return unauth
session = request.ctx.db_session
try:
limit = int(request.args.get('limit') or 50)
except (TypeError, ValueError):
limit = 50
limit = max(1, min(limit, 200))
try:
offset = int(request.args.get('offset') or 0)
except (TypeError, ValueError):
offset = 0
offset = max(0, offset)
search = (request.args.get('search') or '').strip()
type_param = (request.args.get('type') or '').strip()
status_param = (request.args.get('status') or '').strip()
license_type_param = (request.args.get('license_type') or '').strip()
user_id_param = (request.args.get('user_id') or '').strip()
owner_address_param = (request.args.get('owner') or '').strip()
onchain_address_param = (request.args.get('address') or '').strip()
content_hash_param = (request.args.get('content_hash') or '').strip()
filters = []
applied_filters: Dict[str, Any] = {}
if type_param:
type_values = [value.strip() for value in type_param.split(',') if value.strip()]
if type_values:
filters.append(UserContent.type.in_(type_values))
applied_filters['type'] = type_values
if status_param:
status_values = [value.strip() for value in status_param.split(',') if value.strip()]
if status_values:
filters.append(UserContent.status.in_(status_values))
applied_filters['status'] = status_values
if license_type_param:
lt_values: List[int] = []
for part in license_type_param.split(','):
part = part.strip()
if not part:
continue
try:
lt_values.append(int(part))
except ValueError:
continue
if lt_values:
license_type_expr = cast(UserContent.meta['license_type'].astext, Integer)
filters.append(license_type_expr.in_(lt_values))
applied_filters['license_type'] = lt_values
if user_id_param:
try:
filters.append(UserContent.user_id == int(user_id_param))
applied_filters['user_id'] = int(user_id_param)
except ValueError:
pass
if owner_address_param:
filters.append(UserContent.owner_address.ilike(f"%{owner_address_param}%"))
applied_filters['owner'] = owner_address_param
if onchain_address_param:
filters.append(UserContent.onchain_address.ilike(f"%{onchain_address_param}%"))
applied_filters['address'] = onchain_address_param
if content_hash_param:
stored_target = (await session.execute(
select(StoredContent.id).where(StoredContent.hash == content_hash_param)
)).scalars().first()
if stored_target is None:
payload = {
'total': 0,
'limit': limit,
'offset': offset,
'search': search or None,
'filters': {**applied_filters, 'content_hash': content_hash_param},
'items': [],
'counts': {
'status': {},
'type': {},
'license_type': {},
},
'has_more': False,
}
return response.json(payload)
filters.append(UserContent.content_id == int(stored_target))
applied_filters['content_hash'] = content_hash_param
if search:
search_like = f"%{search}%"
clauses = [
cast(UserContent.id, String).ilike(search_like),
UserContent.onchain_address.ilike(search_like),
UserContent.owner_address.ilike(search_like),
]
filters.append(or_(*clauses))
total_stmt = select(func.count()).select_from(UserContent)
if filters:
total_stmt = total_stmt.where(and_(*filters))
total = (await session.execute(total_stmt)).scalar_one()
base_payload = {
'total': int(total or 0),
'limit': limit,
'offset': offset,
'search': search or None,
'filters': applied_filters,
'has_more': (offset + limit) < int(total or 0),
}
if not total:
base_payload.update({
'items': [],
'counts': {
'status': {},
'type': {},
'license_type': {},
},
})
return response.json(base_payload)
query_stmt = (
select(UserContent)
.order_by(UserContent.created.desc())
.offset(offset)
.limit(limit)
)
if filters:
query_stmt = query_stmt.where(and_(*filters))
license_rows = (await session.execute(query_stmt)).scalars().all()
user_ids = [row.user_id for row in license_rows if row.user_id is not None]
content_ids = [row.content_id for row in license_rows if row.content_id is not None]
wallet_ids = [row.wallet_connection_id for row in license_rows if row.wallet_connection_id is not None]
users_map: Dict[int, User] = {}
if user_ids:
user_records = (await session.execute(
select(User).where(User.id.in_(user_ids))
)).scalars().all()
for user in user_records:
users_map[user.id] = user
wallet_map: Dict[int, WalletConnection] = {}
if wallet_ids:
wallet_records = (await session.execute(
select(WalletConnection).where(WalletConnection.id.in_(wallet_ids))
)).scalars().all()
for wallet in wallet_records:
wallet_map[wallet.id] = wallet
content_map: Dict[int, StoredContent] = {}
if content_ids:
content_records = (await session.execute(
select(StoredContent).where(StoredContent.id.in_(content_ids))
)).scalars().all()
for content in content_records:
content_map[content.id] = content
status_stmt = select(UserContent.status, func.count()).group_by(UserContent.status)
if filters:
status_stmt = status_stmt.where(and_(*filters))
status_counts_rows = (await session.execute(status_stmt)).all()
status_counts = {status or 'unknown': int(count or 0) for status, count in status_counts_rows}
type_stmt = select(UserContent.type, func.count()).group_by(UserContent.type)
if filters:
type_stmt = type_stmt.where(and_(*filters))
type_counts_rows = (await session.execute(type_stmt)).all()
type_counts = {ctype or 'unknown': int(count or 0) for ctype, count in type_counts_rows}
license_type_expr = func.coalesce(cast(UserContent.meta['license_type'].astext, Integer), -1)
license_type_stmt = select(license_type_expr.label('license_type'), func.count()).group_by('license_type')
if filters:
license_type_stmt = license_type_stmt.where(and_(*filters))
license_type_counts_rows = (await session.execute(license_type_stmt)).all()
license_type_counts: Dict[str, int] = {}
for lt_value, count in license_type_counts_rows:
key = 'unknown' if lt_value in (None, -1) else str(int(lt_value))
license_type_counts[key] = int(count or 0)
items: List[Dict[str, Any]] = []
for license_row in license_rows:
meta = license_row.meta or {}
license_type_value = meta.get('license_type')
owner_user = users_map.get(license_row.user_id)
wallet_connection = wallet_map.get(license_row.wallet_connection_id)
stored_content = content_map.get(license_row.content_id)
owner_payload = None
if owner_user:
owner_meta = owner_user.meta or {}
owner_payload = {
'id': owner_user.id,
'telegram_id': owner_user.telegram_id,
'username': owner_user.username,
'first_name': owner_meta.get('first_name'),
'last_name': owner_meta.get('last_name'),
}
wallet_payload = None
if wallet_connection:
wallet_payload = {
'id': wallet_connection.id,
'address': wallet_connection.wallet_address,
'network': wallet_connection.network,
'invalidated': wallet_connection.invalidated,
'created_at': _format_dt(wallet_connection.created),
'updated_at': _format_dt(wallet_connection.updated),
}
content_payload = None
if stored_content:
stored_meta = stored_content.meta or {}
metadata_candidate = stored_meta.get('metadata') if isinstance(stored_meta.get('metadata'), dict) else {}
title_candidates = [
stored_meta.get('title'),
metadata_candidate.get('name') if isinstance(metadata_candidate, dict) else None,
stored_meta.get('license', {}).get('title') if isinstance(stored_meta.get('license'), dict) else None,
]
title_value = next((value for value in title_candidates if isinstance(value, str) and value.strip()), None)
try:
cid_value = stored_content.cid.serialize_v2()
except Exception:
cid_value = None
content_payload = {
'id': stored_content.id,
'hash': stored_content.hash,
'cid': cid_value,
'title': title_value or stored_content.hash,
'type': stored_content.type,
'owner_address': stored_content.owner_address,
'onchain_index': stored_content.onchain_index,
'user_id': stored_content.user_id,
'download_url': stored_content.web_url,
}
items.append({
'id': license_row.id,
'type': license_row.type,
'status': license_row.status,
'license_type': license_type_value,
'onchain_address': license_row.onchain_address,
'owner_address': license_row.owner_address,
'user': owner_payload,
'wallet_connection': wallet_payload,
'content': content_payload,
'created_at': _format_dt(license_row.created),
'updated_at': _format_dt(license_row.updated),
'meta': meta,
'links': {
'tonviewer': f"https://tonviewer.com/{license_row.onchain_address}" if license_row.onchain_address else None,
'content_view': f"{PROJECT_HOST}/api/v1/content.view/{license_row.onchain_address}" if license_row.onchain_address else None,
},
})
base_payload.update({
'items': items,
'counts': {
'status': status_counts,
'type': type_counts,
'license_type': license_type_counts,
},
})
return response.json(base_payload)
async def s_api_v1_admin_stars(request):
if (unauth := _ensure_admin(request)):
return unauth
session = request.ctx.db_session
try:
limit = int(request.args.get('limit') or 50)
except (TypeError, ValueError):
limit = 50
limit = max(1, min(limit, 200))
try:
offset = int(request.args.get('offset') or 0)
except (TypeError, ValueError):
offset = 0
offset = max(0, offset)
search = (request.args.get('search') or '').strip()
type_param = (request.args.get('type') or '').strip()
paid_param = _parse_bool_arg(request.args.get('paid'))
user_id_param = (request.args.get('user_id') or '').strip()
content_hash_param = (request.args.get('content_hash') or '').strip()
filters = []
applied_filters: Dict[str, Any] = {}
if paid_param is True:
filters.append(StarsInvoice.paid.is_(True))
applied_filters['paid'] = True
elif paid_param is False:
filters.append(or_(StarsInvoice.paid.is_(False), StarsInvoice.paid.is_(None)))
applied_filters['paid'] = False
if type_param:
type_values = [value.strip() for value in type_param.split(',') if value.strip()]
if type_values:
filters.append(StarsInvoice.type.in_(type_values))
applied_filters['type'] = type_values
if user_id_param:
try:
filters.append(StarsInvoice.user_id == int(user_id_param))
applied_filters['user_id'] = int(user_id_param)
except ValueError:
pass
if content_hash_param:
filters.append(StarsInvoice.content_hash == content_hash_param)
applied_filters['content_hash'] = content_hash_param
if search:
search_like = f"%{search}%"
clauses = [
StarsInvoice.external_id.ilike(search_like),
StarsInvoice.invoice_url.ilike(search_like),
cast(StarsInvoice.id, String).ilike(search_like),
]
filters.append(or_(*clauses))
total_stmt = select(func.count()).select_from(StarsInvoice)
if filters:
total_stmt = total_stmt.where(and_(*filters))
total = (await session.execute(total_stmt)).scalar_one()
base_payload = {
'total': int(total or 0),
'limit': limit,
'offset': offset,
'search': search or None,
'filters': applied_filters,
'has_more': (offset + limit) < int(total or 0),
}
if not total:
base_payload.update({
'items': [],
'stats': {
'total': 0,
'paid': 0,
'unpaid': 0,
'amount_total': 0,
'amount_paid': 0,
'amount_unpaid': 0,
'by_type': {},
},
})
return response.json(base_payload)
query_stmt = (
select(StarsInvoice)
.order_by(StarsInvoice.created.desc())
.offset(offset)
.limit(limit)
)
if filters:
query_stmt = query_stmt.where(and_(*filters))
invoice_rows = (await session.execute(query_stmt)).scalars().all()
user_ids = [row.user_id for row in invoice_rows if row.user_id is not None]
content_hashes = [row.content_hash for row in invoice_rows if row.content_hash]
users_map: Dict[int, User] = {}
if user_ids:
user_records = (await session.execute(select(User).where(User.id.in_(user_ids)))).scalars().all()
for user in user_records:
users_map[user.id] = user
content_map: Dict[str, StoredContent] = {}
if content_hashes:
content_records = (await session.execute(
select(StoredContent).where(StoredContent.hash.in_(content_hashes))
)).scalars().all()
for content in content_records:
content_map[content.hash] = content
stats_stmt = select(
func.count().label('total'),
func.sum(case((StarsInvoice.paid.is_(True), 1), else_=0)).label('paid'),
func.sum(StarsInvoice.amount).label('amount_total'),
func.sum(case((StarsInvoice.paid.is_(True), StarsInvoice.amount), else_=0)).label('amount_paid'),
)
if filters:
stats_stmt = stats_stmt.where(and_(*filters))
stats_row = (await session.execute(stats_stmt)).first()
total_filtered = int((stats_row.total if stats_row else 0) or 0)
paid_count = int((stats_row.paid if stats_row else 0) or 0)
amount_total_value = int((stats_row.amount_total if stats_row else 0) or 0)
amount_paid_value = int((stats_row.amount_paid if stats_row else 0) or 0)
unpaid_count = max(total_filtered - paid_count, 0)
amount_unpaid_value = max(amount_total_value - amount_paid_value, 0)
type_stats_stmt = select(
StarsInvoice.type,
func.count().label('total'),
func.sum(case((StarsInvoice.paid.is_(True), 1), else_=0)).label('paid'),
func.sum(StarsInvoice.amount).label('amount_total'),
func.sum(case((StarsInvoice.paid.is_(True), StarsInvoice.amount), else_=0)).label('amount_paid'),
).group_by(StarsInvoice.type)
if filters:
type_stats_stmt = type_stats_stmt.where(and_(*filters))
type_stats_rows = (await session.execute(type_stats_stmt)).all()
type_stats: Dict[str, Dict[str, int]] = {}
for invoice_type, total_cnt, paid_cnt, amt_total, amt_paid in type_stats_rows:
total_value = int(total_cnt or 0)
paid_value = int(paid_cnt or 0)
amt_total_value = int(amt_total or 0)
amt_paid_value = int(amt_paid or 0)
type_stats[invoice_type or 'unknown'] = {
'total': total_value,
'paid': paid_value,
'unpaid': max(total_value - paid_value, 0),
'amount_total': amt_total_value,
'amount_paid': amt_paid_value,
'amount_unpaid': max(amt_total_value - amt_paid_value, 0),
}
items: List[Dict[str, Any]] = []
for invoice in invoice_rows:
user_payload = None
if invoice.user_id and invoice.user_id in users_map:
user = users_map[invoice.user_id]
user_meta = user.meta or {}
user_payload = {
'id': user.id,
'telegram_id': user.telegram_id,
'username': user.username,
'first_name': user_meta.get('first_name'),
'last_name': user_meta.get('last_name'),
}
content_payload = None
if invoice.content_hash and invoice.content_hash in content_map:
stored_content = content_map[invoice.content_hash]
stored_meta = stored_content.meta or {}
metadata_candidate = stored_meta.get('metadata') if isinstance(stored_meta.get('metadata'), dict) else {}
title_candidates = [
stored_meta.get('title'),
metadata_candidate.get('name') if isinstance(metadata_candidate, dict) else None,
stored_meta.get('license', {}).get('title') if isinstance(stored_meta.get('license'), dict) else None,
]
title_value = next((value for value in title_candidates if isinstance(value, str) and value.strip()), None)
try:
cid_value = stored_content.cid.serialize_v2()
except Exception:
cid_value = None
content_payload = {
'id': stored_content.id,
'hash': stored_content.hash,
'cid': cid_value,
'title': title_value or stored_content.hash,
'onchain_index': stored_content.onchain_index,
'owner_address': stored_content.owner_address,
'user_id': stored_content.user_id,
'download_url': stored_content.web_url,
}
items.append({
'id': invoice.id,
'external_id': invoice.external_id,
'type': invoice.type,
'amount': invoice.amount,
'paid': bool(invoice.paid),
'invoice_url': invoice.invoice_url,
'created_at': _format_dt(invoice.created),
'user': user_payload,
'content': content_payload,
'status': 'paid' if invoice.paid else 'pending',
})
base_payload.update({
'items': items,
'stats': {
'total': total_filtered,
'paid': paid_count,
'unpaid': unpaid_count,
'amount_total': amount_total_value,
'amount_paid': amount_paid_value,
'amount_unpaid': amount_unpaid_value,
'by_type': type_stats,
},
})
return response.json(base_payload)
async def s_api_v1_admin_system(request):
if (unauth := _ensure_admin(request)):
return unauth