Spaces:
Running
Running
| """superadmin/routers/grants.py — Subscription grant management routes. | |
| Routes: | |
| GET /grants | |
| POST /grants/{grant_id}/revoke | |
| POST /grants/{grant_id}/bonus-tokens | |
| POST /grants/{grant_id}/extend | |
| POST /grants/{grant_id}/reset-tokens | |
| POST /grant (convenience: grant by email) | |
| """ | |
| from datetime import datetime, timezone | |
| from fastapi import APIRouter, Depends, HTTPException, Query | |
| from sqlalchemy import select | |
| from sqlalchemy.ext.asyncio import AsyncSession | |
| from app.dependencies import get_db, get_current_superadmin, get_redis | |
| from app.models.client_access import ClientAccess | |
| from app.models.user import User | |
| from app.schemas.admin import GrantByEmailRequest | |
| from app.schemas.superadmin import ( | |
| RevokeAccessRequest, | |
| AddBonusTokensRequest, | |
| ExtendSubscriptionRequest, | |
| ) | |
| from app.services.superadmin_service import SuperAdminService | |
| from app.superadmin.routers._utils import _err | |
| router = APIRouter() | |
| async def list_all_grants( | |
| limit: int = Query(100, ge=1, le=500), | |
| superadmin=Depends(get_current_superadmin), | |
| db: AsyncSession = Depends(get_db), | |
| ): | |
| """List all subscription grants across all authors with token usage details.""" | |
| try: | |
| result = await db.execute( | |
| select(ClientAccess, User) | |
| .join(User, ClientAccess.author_id == User.id) | |
| .order_by(ClientAccess.granted_at.desc()) | |
| .limit(limit) | |
| ) | |
| rows = result.all() | |
| now_naive = datetime.now(timezone.utc).replace(tzinfo=None) | |
| grants = [] | |
| for access, user in rows: | |
| total_budget = access.token_budget + access.bonus_tokens | |
| pct = round((access.tokens_used / total_budget * 100), 1) if total_budget > 0 else 0 | |
| exp = access.expires_at | |
| if exp and exp.tzinfo is not None: | |
| exp = exp.replace(tzinfo=None) | |
| is_expired = exp < now_naive if exp else True | |
| grants.append({ | |
| "grant_id": access.id, | |
| "author_id": access.author_id, | |
| "author_email": user.email, | |
| "author_name": user.full_name, | |
| "plan": access.plan, | |
| "token_budget": access.token_budget, | |
| "bonus_tokens": access.bonus_tokens, | |
| "total_budget": total_budget, | |
| "tokens_used": access.tokens_used, | |
| "tokens_remaining": max(0, total_budget - access.tokens_used), | |
| "usage_pct": pct, | |
| "granted_at": access.granted_at.isoformat() if access.granted_at else None, | |
| "expires_at": access.expires_at.isoformat() if access.expires_at else None, | |
| "is_revoked": access.is_revoked, | |
| "is_expired": is_expired, | |
| "is_active": not access.is_revoked and not is_expired, | |
| "auto_renew": access.auto_renew, | |
| "notes": access.notes, | |
| }) | |
| return {"grants": grants, "count": len(grants)} | |
| except Exception as e: | |
| return _err(e, "list_all_grants") | |
| async def revoke_access( | |
| grant_id: str, | |
| payload: RevokeAccessRequest, | |
| superadmin=Depends(get_current_superadmin), | |
| db: AsyncSession = Depends(get_db), | |
| redis=Depends(get_redis), | |
| ): | |
| """Revoke a subscription instantly (Redis blacklist).""" | |
| try: | |
| await SuperAdminService(db, redis).revoke_access( | |
| actor=superadmin, grant_id=grant_id, reason=payload.reason, | |
| ) | |
| return {"message": "Access revoked"} | |
| except ValueError as e: | |
| raise HTTPException(status_code=400, detail=str(e)) | |
| except Exception as e: | |
| return _err(e, "revoke_access") | |
| async def add_bonus_tokens( | |
| grant_id: str, | |
| payload: AddBonusTokensRequest, | |
| superadmin=Depends(get_current_superadmin), | |
| db: AsyncSession = Depends(get_db), | |
| redis=Depends(get_redis), | |
| ): | |
| """Add bonus tokens without changing subscription expiry.""" | |
| try: | |
| updated = await SuperAdminService(db, redis).add_bonus_tokens( | |
| actor=superadmin, grant_id=grant_id, bonus_tokens=payload.bonus_tokens, | |
| ) | |
| return { | |
| "message": "Bonus tokens added", | |
| "new_bonus_tokens": updated.bonus_tokens if updated else 0, | |
| } | |
| except Exception as e: | |
| return _err(e, "add_bonus_tokens") | |
| async def extend_subscription( | |
| grant_id: str, | |
| payload: ExtendSubscriptionRequest, | |
| superadmin=Depends(get_current_superadmin), | |
| db: AsyncSession = Depends(get_db), | |
| redis=Depends(get_redis), | |
| ): | |
| """Extend subscription expiry date.""" | |
| try: | |
| updated = await SuperAdminService(db, redis).extend_subscription( | |
| actor=superadmin, grant_id=grant_id, extend_days=payload.extend_days, | |
| ) | |
| return { | |
| "message": "Subscription extended", | |
| "new_expires_at": updated.expires_at.isoformat() if updated else None, | |
| } | |
| except Exception as e: | |
| return _err(e, "extend_subscription") | |
| async def reset_token_usage( | |
| grant_id: str, | |
| superadmin=Depends(get_current_superadmin), | |
| db: AsyncSession = Depends(get_db), | |
| redis=Depends(get_redis), | |
| ): | |
| """Reset tokens_used to 0 for a subscription (fresh start).""" | |
| try: | |
| updated = await SuperAdminService(db, redis).reset_token_usage( | |
| actor=superadmin, grant_id=grant_id, | |
| ) | |
| return { | |
| "message": "Token usage reset to 0", | |
| "grant_id": grant_id, | |
| "tokens_used": updated.tokens_used if updated else 0, | |
| } | |
| except Exception as e: | |
| return _err(e, "reset_token_usage") | |
| async def grant_by_email( | |
| body: "GrantByEmailRequest", | |
| superadmin=Depends(get_current_superadmin), | |
| db: AsyncSession = Depends(get_db), | |
| redis=Depends(get_redis), | |
| ): | |
| """Convenience: grant access by author email. R-029: Validated via Pydantic schema.""" | |
| try: | |
| email = body.author_email | |
| result = await db.execute(select(User).where(User.email == email)) | |
| author = result.scalar_one_or_none() | |
| if not author: | |
| raise HTTPException(404, f"Author with email '{email}' not found. Register them first.") | |
| plan_map = { | |
| "starter": "monthly", "pro": "quarterly", | |
| "enterprise": "semi_annual", "unlimited": "annual", | |
| } | |
| plan = plan_map.get(body.plan, body.plan) | |
| service = SuperAdminService(db, redis) | |
| result = await service.grant_access( | |
| actor=superadmin, | |
| author_id=author.id, | |
| plan=plan, | |
| auto_renew=False, | |
| notes=body.notes, | |
| custom_token_budget=body.token_budget, | |
| ) | |
| return result | |
| except HTTPException: | |
| raise | |
| except Exception as e: | |
| return _err(e, "grant_by_email") | |