Initial commit: AegisSight-Monitor-Verwaltung
Dieser Commit ist enthalten in:
0
src/routers/__init__.py
Normale Datei
0
src/routers/__init__.py
Normale Datei
70
src/routers/dashboard.py
Normale Datei
70
src/routers/dashboard.py
Normale Datei
@@ -0,0 +1,70 @@
|
||||
"""Dashboard-Statistiken."""
|
||||
from fastapi import APIRouter, Depends
|
||||
from auth import get_current_admin
|
||||
from database import db_dependency
|
||||
import aiosqlite
|
||||
|
||||
router = APIRouter(prefix="/api/dashboard", tags=["dashboard"])
|
||||
|
||||
|
||||
@router.get("/stats")
|
||||
async def get_stats(
|
||||
admin: dict = Depends(get_current_admin),
|
||||
db: aiosqlite.Connection = Depends(db_dependency),
|
||||
):
|
||||
"""Gesamtstatistiken fuer das Dashboard."""
|
||||
cursor = await db.execute("SELECT COUNT(*) as cnt FROM organizations")
|
||||
org_count = (await cursor.fetchone())["cnt"]
|
||||
|
||||
cursor = await db.execute("SELECT COUNT(*) as cnt FROM organizations WHERE is_active = 1")
|
||||
active_orgs = (await cursor.fetchone())["cnt"]
|
||||
|
||||
cursor = await db.execute("SELECT COUNT(*) as cnt FROM users WHERE is_active = 1")
|
||||
active_users = (await cursor.fetchone())["cnt"]
|
||||
|
||||
cursor = await db.execute("SELECT COUNT(*) as cnt FROM users")
|
||||
total_users = (await cursor.fetchone())["cnt"]
|
||||
|
||||
cursor = await db.execute("SELECT COUNT(*) as cnt FROM licenses WHERE status = 'active'")
|
||||
active_licenses = (await cursor.fetchone())["cnt"]
|
||||
|
||||
cursor = await db.execute("SELECT COUNT(*) as cnt FROM incidents")
|
||||
total_incidents = (await cursor.fetchone())["cnt"]
|
||||
|
||||
cursor = await db.execute("SELECT COUNT(*) as cnt FROM incidents WHERE status = 'active'")
|
||||
active_incidents = (await cursor.fetchone())["cnt"]
|
||||
|
||||
# Bald ablaufende Lizenzen (30 Tage)
|
||||
cursor = await db.execute(
|
||||
"""SELECT l.id, l.organization_id, l.license_type, l.valid_until, l.max_users,
|
||||
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', '+30 days')
|
||||
ORDER BY l.valid_until"""
|
||||
)
|
||||
expiring = [dict(row) for row in await cursor.fetchall()]
|
||||
|
||||
# Letzte Aktivitaeten (neue Orgs, neue Nutzer)
|
||||
cursor = await db.execute(
|
||||
"SELECT 'org' as type, name as label, created_at FROM organizations ORDER BY created_at DESC LIMIT 5"
|
||||
)
|
||||
recent_orgs = [dict(row) for row in await cursor.fetchall()]
|
||||
|
||||
cursor = await db.execute(
|
||||
"""SELECT 'user' as type, u.username || ' (' || o.name || ')' as label, u.created_at
|
||||
FROM users u JOIN organizations o ON o.id = u.organization_id
|
||||
ORDER BY u.created_at DESC LIMIT 5"""
|
||||
)
|
||||
recent_users = [dict(row) for row in await cursor.fetchall()]
|
||||
|
||||
return {
|
||||
"organizations": {"total": org_count, "active": active_orgs},
|
||||
"users": {"total": total_users, "active": active_users},
|
||||
"licenses": {"active": active_licenses},
|
||||
"incidents": {"total": total_incidents, "active": active_incidents},
|
||||
"expiring_licenses": expiring,
|
||||
"recent_activity": sorted(recent_orgs + recent_users, key=lambda x: x["created_at"], reverse=True)[:10],
|
||||
}
|
||||
129
src/routers/licenses.py
Normale Datei
129
src/routers/licenses.py
Normale Datei
@@ -0,0 +1,129 @@
|
||||
"""Lizenz-CRUD."""
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from models import LicenseCreate, LicenseResponse
|
||||
from auth import get_current_admin
|
||||
from database import db_dependency
|
||||
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,
|
||||
admin: dict = Depends(get_current_admin),
|
||||
db: aiosqlite.Connection = Depends(db_dependency),
|
||||
):
|
||||
# Org pruefen
|
||||
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
|
||||
await db.execute(
|
||||
"UPDATE licenses SET status = 'revoked' WHERE organization_id = ? AND status = 'active'",
|
||||
(data.organization_id,),
|
||||
)
|
||||
|
||||
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()
|
||||
|
||||
cursor = await db.execute(
|
||||
"""INSERT INTO licenses (organization_id, license_type, max_users, valid_from, valid_until, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'active')""",
|
||||
(data.organization_id, data.license_type, data.max_users, valid_from, valid_until),
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
cursor = await db.execute("SELECT * FROM licenses WHERE id = ?", (cursor.lastrowid,))
|
||||
return dict(await cursor.fetchone())
|
||||
|
||||
|
||||
@router.put("/{license_id}/revoke")
|
||||
async def revoke_license(
|
||||
license_id: int,
|
||||
admin: dict = Depends(get_current_admin),
|
||||
db: aiosqlite.Connection = Depends(db_dependency),
|
||||
):
|
||||
cursor = await db.execute("SELECT * FROM licenses WHERE id = ?", (license_id,))
|
||||
lic = await cursor.fetchone()
|
||||
if not lic:
|
||||
raise HTTPException(status_code=404, detail="Lizenz nicht gefunden")
|
||||
|
||||
await db.execute("UPDATE licenses SET status = 'revoked' WHERE id = ?", (license_id,))
|
||||
await db.commit()
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@router.put("/{license_id}/extend")
|
||||
async def extend_license(
|
||||
license_id: int,
|
||||
days: int = 365,
|
||||
admin: dict = Depends(get_current_admin),
|
||||
db: aiosqlite.Connection = Depends(db_dependency),
|
||||
):
|
||||
cursor = await db.execute("SELECT * FROM licenses WHERE id = ?", (license_id,))
|
||||
lic = await cursor.fetchone()
|
||||
if not lic:
|
||||
raise HTTPException(status_code=404, detail="Lizenz nicht gefunden")
|
||||
|
||||
if lic["valid_until"]:
|
||||
base = datetime.fromisoformat(lic["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()
|
||||
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()]
|
||||
116
src/routers/organizations.py
Normale Datei
116
src/routers/organizations.py
Normale Datei
@@ -0,0 +1,116 @@
|
||||
"""Organisations-CRUD."""
|
||||
from datetime import datetime, timezone
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from models import OrgCreate, OrgUpdate, OrgResponse
|
||||
from auth import get_current_admin
|
||||
from database import db_dependency
|
||||
import aiosqlite
|
||||
|
||||
router = APIRouter(prefix="/api/orgs", tags=["organizations"])
|
||||
|
||||
|
||||
async def _enrich_org(db: aiosqlite.Connection, row: aiosqlite.Row) -> dict:
|
||||
org = dict(row)
|
||||
cursor = await db.execute(
|
||||
"SELECT COUNT(*) as cnt FROM users WHERE organization_id = ? AND is_active = 1",
|
||||
(org["id"],),
|
||||
)
|
||||
org["user_count"] = (await cursor.fetchone())["cnt"]
|
||||
|
||||
cursor = await db.execute(
|
||||
"SELECT license_type, status FROM licenses WHERE organization_id = ? AND status = 'active' ORDER BY created_at DESC LIMIT 1",
|
||||
(org["id"],),
|
||||
)
|
||||
lic = await cursor.fetchone()
|
||||
org["license_status"] = lic["status"] if lic else "none"
|
||||
org["license_type"] = lic["license_type"] if lic else ""
|
||||
return org
|
||||
|
||||
|
||||
@router.get("", response_model=list[OrgResponse])
|
||||
async def list_organizations(
|
||||
admin: dict = Depends(get_current_admin),
|
||||
db: aiosqlite.Connection = Depends(db_dependency),
|
||||
):
|
||||
cursor = await db.execute("SELECT * FROM organizations ORDER BY created_at DESC")
|
||||
rows = await cursor.fetchall()
|
||||
return [await _enrich_org(db, row) for row in rows]
|
||||
|
||||
|
||||
@router.post("", response_model=OrgResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def create_organization(
|
||||
data: OrgCreate,
|
||||
admin: dict = Depends(get_current_admin),
|
||||
db: aiosqlite.Connection = Depends(db_dependency),
|
||||
):
|
||||
# Slug-Duplikat pruefen
|
||||
cursor = await db.execute("SELECT id FROM organizations WHERE slug = ?", (data.slug,))
|
||||
if await cursor.fetchone():
|
||||
raise HTTPException(status_code=400, detail="Slug bereits vergeben")
|
||||
|
||||
now = datetime.now(timezone.utc).isoformat()
|
||||
cursor = await db.execute(
|
||||
"INSERT INTO organizations (name, slug, is_active, created_at, updated_at) VALUES (?, ?, 1, ?, ?)",
|
||||
(data.name, data.slug, now, now),
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
cursor = await db.execute("SELECT * FROM organizations WHERE id = ?", (cursor.lastrowid,))
|
||||
return await _enrich_org(db, await cursor.fetchone())
|
||||
|
||||
|
||||
@router.get("/{org_id}", response_model=OrgResponse)
|
||||
async def get_organization(
|
||||
org_id: int,
|
||||
admin: dict = Depends(get_current_admin),
|
||||
db: aiosqlite.Connection = Depends(db_dependency),
|
||||
):
|
||||
cursor = await db.execute("SELECT * FROM organizations WHERE id = ?", (org_id,))
|
||||
row = await cursor.fetchone()
|
||||
if not row:
|
||||
raise HTTPException(status_code=404, detail="Organisation nicht gefunden")
|
||||
return await _enrich_org(db, row)
|
||||
|
||||
|
||||
@router.put("/{org_id}", response_model=OrgResponse)
|
||||
async def update_organization(
|
||||
org_id: int,
|
||||
data: OrgUpdate,
|
||||
admin: dict = Depends(get_current_admin),
|
||||
db: aiosqlite.Connection = Depends(db_dependency),
|
||||
):
|
||||
cursor = await db.execute("SELECT * FROM organizations WHERE id = ?", (org_id,))
|
||||
row = await cursor.fetchone()
|
||||
if not row:
|
||||
raise HTTPException(status_code=404, detail="Organisation nicht gefunden")
|
||||
|
||||
updates = {}
|
||||
if data.name is not None:
|
||||
updates["name"] = data.name
|
||||
if data.is_active is not None:
|
||||
updates["is_active"] = 1 if data.is_active else 0
|
||||
|
||||
if updates:
|
||||
updates["updated_at"] = datetime.now(timezone.utc).isoformat()
|
||||
set_clause = ", ".join(f"{k} = ?" for k in updates)
|
||||
values = list(updates.values()) + [org_id]
|
||||
await db.execute(f"UPDATE organizations SET {set_clause} WHERE id = ?", values)
|
||||
await db.commit()
|
||||
|
||||
cursor = await db.execute("SELECT * FROM organizations WHERE id = ?", (org_id,))
|
||||
return await _enrich_org(db, await cursor.fetchone())
|
||||
|
||||
|
||||
@router.delete("/{org_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_organization(
|
||||
org_id: int,
|
||||
admin: dict = Depends(get_current_admin),
|
||||
db: aiosqlite.Connection = Depends(db_dependency),
|
||||
):
|
||||
cursor = await db.execute("SELECT * FROM organizations WHERE id = ?", (org_id,))
|
||||
if not await cursor.fetchone():
|
||||
raise HTTPException(status_code=404, detail="Organisation nicht gefunden")
|
||||
|
||||
# Kaskadierendes Loeschen
|
||||
await db.execute("DELETE FROM organizations WHERE id = ?", (org_id,))
|
||||
await db.commit()
|
||||
163
src/routers/users.py
Normale Datei
163
src/routers/users.py
Normale Datei
@@ -0,0 +1,163 @@
|
||||
"""Nutzer-Verwaltung pro Organisation."""
|
||||
import secrets
|
||||
import string
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from models import UserCreate, UserResponse
|
||||
from auth import get_current_admin
|
||||
from database import db_dependency
|
||||
from config import MAGIC_LINK_BASE_URL, MAGIC_LINK_EXPIRE_MINUTES
|
||||
import aiosqlite
|
||||
|
||||
router = APIRouter(prefix="/api/users", tags=["users"])
|
||||
|
||||
|
||||
@router.get("", response_model=list[UserResponse])
|
||||
async def list_users(
|
||||
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 users WHERE organization_id = ? ORDER BY created_at",
|
||||
(org_id,),
|
||||
)
|
||||
else:
|
||||
cursor = await db.execute("SELECT * FROM users ORDER BY organization_id, created_at")
|
||||
return [dict(row) for row in await cursor.fetchall()]
|
||||
|
||||
|
||||
@router.post("", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def create_user(
|
||||
data: UserCreate,
|
||||
org_id: int = None,
|
||||
admin: dict = Depends(get_current_admin),
|
||||
db: aiosqlite.Connection = Depends(db_dependency),
|
||||
):
|
||||
if not org_id:
|
||||
raise HTTPException(status_code=400, detail="org_id Parameter erforderlich")
|
||||
|
||||
email = data.email.lower().strip()
|
||||
|
||||
# Org pruefen
|
||||
cursor = await db.execute("SELECT id, name FROM organizations WHERE id = ?", (org_id,))
|
||||
org = await cursor.fetchone()
|
||||
if not org:
|
||||
raise HTTPException(status_code=404, detail="Organisation nicht gefunden")
|
||||
|
||||
# Nutzer-Limit pruefen
|
||||
cursor = await db.execute(
|
||||
"SELECT max_users FROM licenses WHERE organization_id = ? AND status = 'active' ORDER BY created_at DESC LIMIT 1",
|
||||
(org_id,),
|
||||
)
|
||||
lic = await cursor.fetchone()
|
||||
if lic:
|
||||
cursor = await db.execute(
|
||||
"SELECT COUNT(*) as cnt FROM users WHERE organization_id = ? AND is_active = 1",
|
||||
(org_id,),
|
||||
)
|
||||
current = (await cursor.fetchone())["cnt"]
|
||||
if current >= lic["max_users"]:
|
||||
raise HTTPException(status_code=400, detail=f"Nutzer-Limit erreicht ({current}/{lic['max_users']})")
|
||||
|
||||
# E-Mail-Duplikat
|
||||
cursor = await db.execute("SELECT id FROM users WHERE LOWER(email) = ?", (email,))
|
||||
if await cursor.fetchone():
|
||||
raise HTTPException(status_code=400, detail="E-Mail bereits vergeben")
|
||||
|
||||
now = datetime.now(timezone.utc).isoformat()
|
||||
cursor = await db.execute(
|
||||
"""INSERT INTO users (email, username, password_hash, organization_id, role, is_active, created_at)
|
||||
VALUES (?, ?, '', ?, ?, 1, ?)""",
|
||||
(email, data.username, org_id, data.role, now),
|
||||
)
|
||||
user_id = cursor.lastrowid
|
||||
|
||||
# Magic Link fuer Einladung erstellen
|
||||
token = secrets.token_urlsafe(48)
|
||||
code = ''.join(secrets.choice(string.digits) for _ in range(6))
|
||||
expires_at = (datetime.now(timezone.utc) + timedelta(hours=48)).isoformat()
|
||||
|
||||
await db.execute(
|
||||
"""INSERT INTO magic_links (email, token, code, purpose, user_id, expires_at)
|
||||
VALUES (?, ?, ?, 'invite', ?, ?)""",
|
||||
(email, token, code, user_id, expires_at),
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
# Einladungs-E-Mail senden
|
||||
try:
|
||||
from email_utils.sender import send_email
|
||||
from email_utils.templates import invite_email
|
||||
link = f"{MAGIC_LINK_BASE_URL}/auth/verify?token={token}"
|
||||
subject, html = invite_email(data.username, org["name"], code, link)
|
||||
await send_email(email, subject, html)
|
||||
except Exception:
|
||||
pass # E-Mail-Fehler nicht fatal
|
||||
|
||||
cursor = await db.execute("SELECT * FROM users WHERE id = ?", (user_id,))
|
||||
return dict(await cursor.fetchone())
|
||||
|
||||
|
||||
@router.put("/{user_id}/deactivate")
|
||||
async def deactivate_user(
|
||||
user_id: int,
|
||||
admin: dict = Depends(get_current_admin),
|
||||
db: aiosqlite.Connection = Depends(db_dependency),
|
||||
):
|
||||
cursor = await db.execute("SELECT id FROM users WHERE id = ?", (user_id,))
|
||||
if not await cursor.fetchone():
|
||||
raise HTTPException(status_code=404, detail="Nutzer nicht gefunden")
|
||||
|
||||
await db.execute("UPDATE users SET is_active = 0 WHERE id = ?", (user_id,))
|
||||
await db.commit()
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@router.put("/{user_id}/activate")
|
||||
async def activate_user(
|
||||
user_id: int,
|
||||
admin: dict = Depends(get_current_admin),
|
||||
db: aiosqlite.Connection = Depends(db_dependency),
|
||||
):
|
||||
cursor = await db.execute("SELECT id FROM users WHERE id = ?", (user_id,))
|
||||
if not await cursor.fetchone():
|
||||
raise HTTPException(status_code=404, detail="Nutzer nicht gefunden")
|
||||
|
||||
await db.execute("UPDATE users SET is_active = 1 WHERE id = ?", (user_id,))
|
||||
await db.commit()
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@router.put("/{user_id}/role")
|
||||
async def change_role(
|
||||
user_id: int,
|
||||
role: str = "member",
|
||||
admin: dict = Depends(get_current_admin),
|
||||
db: aiosqlite.Connection = Depends(db_dependency),
|
||||
):
|
||||
if role not in ("org_admin", "member"):
|
||||
raise HTTPException(status_code=400, detail="Ungueltige Rolle")
|
||||
|
||||
cursor = await db.execute("SELECT id FROM users WHERE id = ?", (user_id,))
|
||||
if not await cursor.fetchone():
|
||||
raise HTTPException(status_code=404, detail="Nutzer nicht gefunden")
|
||||
|
||||
await db.execute("UPDATE users SET role = ? WHERE id = ?", (role, user_id))
|
||||
await db.commit()
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_user(
|
||||
user_id: int,
|
||||
admin: dict = Depends(get_current_admin),
|
||||
db: aiosqlite.Connection = Depends(db_dependency),
|
||||
):
|
||||
cursor = await db.execute("SELECT id FROM users WHERE id = ?", (user_id,))
|
||||
if not await cursor.fetchone():
|
||||
raise HTTPException(status_code=404, detail="Nutzer nicht gefunden")
|
||||
|
||||
await db.execute("DELETE FROM users WHERE id = ?", (user_id,))
|
||||
await db.commit()
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren