Hiren122's picture
Create app.py
1e2860e verified
"""
============================================================
LICENSE SERVER β€” Deploy this on a SEPARATE HuggingFace Space
This server:
- Stores valid license keys (in licenses.json)
- Validates keys submitted by clients
- Lets you add/revoke keys via admin API
Set the env var ADMIN_SECRET in your HF Space secrets.
============================================================
"""
import os
import json
import uuid
import hashlib
from datetime import datetime, timezone
from pathlib import Path
from fastapi import FastAPI, HTTPException, Header, Depends
from fastapi.responses import JSONResponse
import uvicorn
# ============================================================
# CONFIG
# ============================================================
ADMIN_SECRET = os.environ.get("ADMIN_SECRET", "change-me-super-secret")
LICENSE_FILE = Path("licenses.json")
PORT = int(os.environ.get("PORT", 7860))
# ============================================================
# LICENSE STORAGE
# ============================================================
def load_licenses() -> dict:
"""Load licenses from file."""
if LICENSE_FILE.exists():
try:
return json.loads(LICENSE_FILE.read_text())
except Exception:
return {}
return {}
def save_licenses(data: dict):
"""Save licenses to file."""
LICENSE_FILE.write_text(json.dumps(data, indent=2))
def hash_key(key: str) -> str:
"""Hash a license key for storage (never store raw keys)."""
return hashlib.sha256(key.encode()).hexdigest()
# ============================================================
# FASTAPI APP
# ============================================================
app = FastAPI(title="License Server", docs_url=None, redoc_url=None)
# ---- Admin auth dependency ----
def require_admin(x_admin_secret: str = Header(...)):
if x_admin_secret != ADMIN_SECRET:
raise HTTPException(status_code=403, detail="Invalid admin secret")
# ============================================================
# PUBLIC ENDPOINT β€” clients call this to validate their key
# ============================================================
@app.get("/validate")
async def validate_license(key: str):
"""
Validate a license key.
Returns {"valid": true/false, "message": "..."}
"""
licenses = load_licenses()
key_hash = hash_key(key)
if key_hash not in licenses:
return JSONResponse({"valid": False, "message": "Invalid license key."})
entry = licenses[key_hash]
# Check if revoked
if entry.get("revoked"):
return JSONResponse({"valid": False, "message": "License key has been revoked."})
# Check expiry
expires_at = entry.get("expires_at")
if expires_at:
expiry_dt = datetime.fromisoformat(expires_at)
if datetime.now(timezone.utc) > expiry_dt:
return JSONResponse({"valid": False, "message": "License key has expired."})
# Update last_seen
entry["last_seen"] = datetime.now(timezone.utc).isoformat()
licenses[key_hash] = entry
save_licenses(licenses)
return JSONResponse({
"valid": True,
"message": "License key is valid.",
"label": entry.get("label", ""),
"expires_at": expires_at
})
# ============================================================
# ADMIN ENDPOINTS β€” only you can call these
# ============================================================
@app.post("/admin/generate", dependencies=[Depends(require_admin)])
async def generate_key(label: str = "", expires_days: int = 0):
"""
Generate a new license key.
- label: optional note (e.g. customer name)
- expires_days: 0 = never expires
"""
key = str(uuid.uuid4()).replace("-", "").upper()
key_formatted = f"LMA-{key[:8]}-{key[8:16]}-{key[16:24]}"
key_hash = hash_key(key_formatted)
expires_at = None
if expires_days > 0:
from datetime import timedelta
expires_at = (datetime.now(timezone.utc) + timedelta(days=expires_days)).isoformat()
licenses = load_licenses()
licenses[key_hash] = {
"label": label,
"created_at": datetime.now(timezone.utc).isoformat(),
"expires_at": expires_at,
"revoked": False,
"last_seen": None
}
save_licenses(licenses)
return {"key": key_formatted, "label": label, "expires_at": expires_at}
@app.post("/admin/revoke", dependencies=[Depends(require_admin)])
async def revoke_key(key: str):
"""Revoke a license key."""
licenses = load_licenses()
key_hash = hash_key(key)
if key_hash not in licenses:
raise HTTPException(status_code=404, detail="Key not found.")
licenses[key_hash]["revoked"] = True
save_licenses(licenses)
return {"message": f"Key revoked successfully."}
@app.get("/admin/list", dependencies=[Depends(require_admin)])
async def list_keys():
"""List all license keys (hashed, with metadata)."""
licenses = load_licenses()
return {"total": len(licenses), "licenses": licenses}
@app.get("/health")
async def health():
return {"status": "ok"}
# ============================================================
if __name__ == "__main__":
print("=" * 50)
print("πŸ”‘ License Server Starting...")
print(f" Port: {PORT}")
print(f" Licenses file: {LICENSE_FILE.absolute()}")
print("=" * 50)
uvicorn.run(app, host="0.0.0.0", port=PORT)