"""Lizenz-CRUD.""" from datetime import datetime, timedelta, timezone from fastapi import APIRouter, Depends, HTTPException, status, Request from models import LicenseCreate, LicenseResponse from auth import get_current_admin from database import db_dependency from audit import log_action, get_client_ip, row_to_dict import aiosqlite router = APIRouter(prefix="/api/licenses", tags=["licenses"]) @router.get("", response_model=list[LicenseResponse]) async def list_licenses( org_id: int = None, admin: dict = Depends(get_current_admin), db: aiosqlite.Connection = Depends(db_dependency), ): if org_id: cursor = await db.execute( "SELECT * FROM licenses WHERE organization_id = ? ORDER BY created_at DESC", (org_id,), ) else: cursor = await db.execute("SELECT * FROM licenses ORDER BY created_at DESC") return [dict(row) for row in await cursor.fetchall()] @router.post("", response_model=LicenseResponse, status_code=status.HTTP_201_CREATED) async def create_license( data: LicenseCreate, request: Request, admin: dict = Depends(get_current_admin), db: aiosqlite.Connection = Depends(db_dependency), ): cursor = await db.execute( "SELECT id FROM organizations WHERE id = ?", (data.organization_id,) ) if not await cursor.fetchone(): raise HTTPException(status_code=404, detail="Organisation nicht gefunden") # Bestehende aktive Lizenz widerrufen + Snapshot fuer Audit cursor = await db.execute( "SELECT * FROM licenses WHERE organization_id = ? AND status = 'active'", (data.organization_id,), ) revoked_lics = [dict(r) for r in await cursor.fetchall()] if revoked_lics: await db.execute( "UPDATE licenses SET status = 'revoked' WHERE organization_id = ? AND status = 'active'", (data.organization_id,), ) for old_lic in revoked_lics: new_lic = dict(old_lic) new_lic["status"] = "revoked" await log_action( db, admin, get_client_ip(request), action="update", resource_type="license", resource_id=old_lic["id"], before=old_lic, after=new_lic, ) now = datetime.now(timezone.utc) valid_from = now.isoformat() valid_until = None if data.license_type == "permanent": valid_until = None elif data.duration_days: valid_until = (now + timedelta(days=data.duration_days)).isoformat() elif data.license_type == "trial": valid_until = (now + timedelta(days=14)).isoformat() elif data.license_type == "annual": valid_until = (now + timedelta(days=365)).isoformat() # Bei unlimited_budget: Credits/Cost/Budget ignorieren if data.unlimited_budget: token_budget_usd = None credits_total = None cost_per_credit = None else: token_budget_usd = data.token_budget_usd credits_total = data.credits_total cost_per_credit = data.cost_per_credit cursor = await db.execute( """INSERT INTO licenses (organization_id, license_type, max_users, valid_from, valid_until, status, token_budget_usd, credits_total, cost_per_credit, budget_warning_percent, unlimited_budget) VALUES (?, ?, ?, ?, ?, 'active', ?, ?, ?, ?, ?)""", (data.organization_id, data.license_type, data.max_users, valid_from, valid_until, token_budget_usd, credits_total, cost_per_credit, data.budget_warning_percent, 1 if data.unlimited_budget else 0), ) lic_id = cursor.lastrowid await db.commit() cursor = await db.execute("SELECT * FROM licenses WHERE id = ?", (lic_id,)) new_lic = dict(await cursor.fetchone()) await log_action( db, admin, get_client_ip(request), action="create", resource_type="license", resource_id=lic_id, after=new_lic, ) return new_lic @router.put("/{license_id}/revoke") async def revoke_license( license_id: int, request: Request, admin: dict = Depends(get_current_admin), db: aiosqlite.Connection = Depends(db_dependency), ): before = await row_to_dict(db, "licenses", license_id) if not before: raise HTTPException(status_code=404, detail="Lizenz nicht gefunden") await db.execute("UPDATE licenses SET status = 'revoked' WHERE id = ?", (license_id,)) await db.commit() after = await row_to_dict(db, "licenses", license_id) await log_action( db, admin, get_client_ip(request), action="update", resource_type="license", resource_id=license_id, before=before, after=after, ) return {"ok": True} @router.put("/{license_id}/extend") async def extend_license( license_id: int, request: Request, days: int = 365, admin: dict = Depends(get_current_admin), db: aiosqlite.Connection = Depends(db_dependency), ): before = await row_to_dict(db, "licenses", license_id) if not before: raise HTTPException(status_code=404, detail="Lizenz nicht gefunden") if before.get("valid_until"): base = datetime.fromisoformat(before["valid_until"]) else: base = datetime.now(timezone.utc) new_until = (base + timedelta(days=days)).isoformat() await db.execute( "UPDATE licenses SET valid_until = ?, status = 'active' WHERE id = ?", (new_until, license_id), ) await db.commit() after = await row_to_dict(db, "licenses", license_id) await log_action( db, admin, get_client_ip(request), action="update", resource_type="license", resource_id=license_id, before=before, after=after, ) return {"ok": True, "valid_until": new_until} @router.get("/expiring") async def get_expiring_licenses( days: int = 30, admin: dict = Depends(get_current_admin), db: aiosqlite.Connection = Depends(db_dependency), ): """Lizenzen die in den naechsten X Tagen ablaufen.""" cursor = await db.execute( """SELECT l.*, o.name as org_name FROM licenses l JOIN organizations o ON o.id = l.organization_id WHERE l.status = 'active' AND l.valid_until IS NOT NULL AND l.valid_until < datetime('now', '+' || ? || ' days') ORDER BY l.valid_until""", (days,), ) return [dict(row) for row in await cursor.fetchall()]