"""License management endpoints.""" from datetime import date, datetime, timezone from typing import Optional from fastapi import APIRouter, Depends, HTTPException, Request from pydantic import BaseModel from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, update from app.auth.dependencies import require_super_admin from app.database import get_db from app.models.user import HubUser from app.models.license import License, LicenseStatus from app.models.school import School, SchoolStatus router = APIRouter(prefix="/api/licenses", tags=["licenses"]) class LicenseUpdate(BaseModel): status: Optional[LicenseStatus] = None expires_at: Optional[date] = None max_students: Optional[int] = None notes: Optional[str] = None @router.get("") async def list_licenses( _admin: HubUser = Depends(require_super_admin), db: AsyncSession = Depends(get_db), ): result = await db.execute(select(License)) return [ { "id": l.id, "school_id": l.school_id, "key": l.key, "status": l.status.value, "tier": l.tier, "issued_at": l.issued_at.isoformat(), "expires_at": l.expires_at.isoformat() if l.expires_at else None, "last_validated_at": l.last_validated_at.isoformat() if l.last_validated_at else None, "last_seen_ip": l.last_seen_ip, "max_students": l.max_students, } for l in result.scalars().all() ] @router.put("/{license_id}") async def update_license( license_id: str, body: LicenseUpdate, _admin: HubUser = Depends(require_super_admin), db: AsyncSession = Depends(get_db), ): lic = (await db.execute(select(License).where(License.id == license_id))).scalar_one_or_none() if not lic: raise HTTPException(404, "License not found") for field, value in body.model_dump(exclude_none=True).items(): setattr(lic, field, value) await db.commit() return {"id": lic.id, "status": lic.status.value, "expires_at": lic.expires_at.isoformat() if lic.expires_at else None} @router.post("/{license_id}/revoke") async def revoke_license( license_id: str, _admin: HubUser = Depends(require_super_admin), db: AsyncSession = Depends(get_db), ): lic = (await db.execute(select(License).where(License.id == license_id))).scalar_one_or_none() if not lic: raise HTTPException(404, "License not found") lic.status = LicenseStatus.revoked # Also suspend the school school = (await db.execute(select(School).where(School.id == lic.school_id))).scalar_one_or_none() if school: school.status = SchoolStatus.suspended await db.commit() return {"message": "License revoked"} @router.post("/validate") async def validate_license( request: Request, db: AsyncSession = Depends(get_db), ): """Called by on-prem TapTrack to validate their license key. No auth required — uses key.""" body = await request.json() key: str = body.get("key", "") if not key: raise HTTPException(400, "License key required") lic = (await db.execute(select(License).where(License.key == key))).scalar_one_or_none() if not lic: return {"valid": False, "reason": "Key not found"} if lic.status == LicenseStatus.revoked: return {"valid": False, "reason": "License revoked"} if lic.expires_at and lic.expires_at < date.today(): lic.status = LicenseStatus.expired await db.commit() return {"valid": False, "reason": "License expired", "expired_at": lic.expires_at.isoformat()} # Update validation metadata lic.last_validated_at = datetime.now(timezone.utc) lic.last_seen_ip = request.client.host if request.client else None school = (await db.execute(select(School).where(School.id == lic.school_id))).scalar_one_or_none() await db.commit() return { "valid": True, "school_id": lic.school_id, "school_name": school.name if school else None, "tier": lic.tier, "max_students": lic.max_students, "expires_at": lic.expires_at.isoformat() if lic.expires_at else None, "sms_sender_name": school.sms_sender_name if school else "SCHOOL", "sms_credits": float(school.sms_credits) if school else 0.0, "features": _tier_features(lic.tier), } @router.get("/school/{school_id}") async def get_school_license( school_id: str, _admin: HubUser = Depends(require_super_admin), db: AsyncSession = Depends(get_db), ): """Get the license for a specific school.""" lic = (await db.execute(select(License).where(License.school_id == school_id))).scalar_one_or_none() if not lic: raise HTTPException(404, "No license found for this school") return { "id": lic.id, "school_id": lic.school_id, "key": lic.key, "status": lic.status.value, "tier": lic.tier, "issued_at": lic.issued_at.isoformat(), "expires_at": lic.expires_at.isoformat() if lic.expires_at else None, "last_validated_at": lic.last_validated_at.isoformat() if lic.last_validated_at else None, "last_seen_ip": lic.last_seen_ip, "max_students": lic.max_students, } def _tier_features(tier: str) -> dict: base = {"sms": True, "reports": True, "websocket": True, "multi_terminal": True} if tier == "premium": base.update({"api_keys": True, "webhooks": True, "bulk_enrollment": True}) elif tier == "basic": base.update({"multi_terminal": False, "api_keys": False, "webhooks": False}) return base