Audit-Log + Brute-Force-Schutz + unlimited_budget + User-Delete-Fix
- Schema-Migration: ON DELETE SET NULL fuer incidents.created_by, magic_links.user_id, network_analyses.created_by (behebt 500er beim User-Loeschen). Neue Spalte licenses.unlimited_budget. Neue Tabellen portal_audit_log, portal_login_attempts. - Audit-Log: alle CREATE/UPDATE/DELETE auf Org/User/Lizenz/Quelle + Login-Events werden mit before/after-Diff in portal_audit_log geschrieben. - Brute-Force-Schutz: 5 Fehlversuche pro IP+Username/15min -> 429 mit Retry-After. - Token-Budget: expliziter Schalter unlimited_budget pro Lizenz. UI zeigt ehrlich >100%-Verbrauch (kein Math.min mehr) und ungebremste Anzeige bei unlimited. - Neuer Audit-Log Tab mit Filter (Aktion/Ressource/Admin/Zeitraum) und Pagination.
Dieser Commit ist enthalten in:
@@ -1,9 +1,10 @@
|
||||
"""Organisations-CRUD."""
|
||||
from datetime import datetime, timezone
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Request
|
||||
from models import OrgCreate, OrgUpdate, OrgResponse
|
||||
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/orgs", tags=["organizations"])
|
||||
@@ -40,10 +41,10 @@ async def list_organizations(
|
||||
@router.post("", response_model=OrgResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def create_organization(
|
||||
data: OrgCreate,
|
||||
request: Request,
|
||||
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")
|
||||
@@ -53,10 +54,17 @@ async def create_organization(
|
||||
"INSERT INTO organizations (name, slug, is_active, created_at, updated_at) VALUES (?, ?, 1, ?, ?)",
|
||||
(data.name, data.slug, now, now),
|
||||
)
|
||||
org_id = cursor.lastrowid
|
||||
await db.commit()
|
||||
|
||||
cursor = await db.execute("SELECT * FROM organizations WHERE id = ?", (cursor.lastrowid,))
|
||||
return await _enrich_org(db, await cursor.fetchone())
|
||||
cursor = await db.execute("SELECT * FROM organizations WHERE id = ?", (org_id,))
|
||||
new_row_obj = await cursor.fetchone()
|
||||
await log_action(
|
||||
db, admin, get_client_ip(request),
|
||||
action="create", resource_type="organization", resource_id=org_id,
|
||||
after=dict(new_row_obj),
|
||||
)
|
||||
return await _enrich_org(db, new_row_obj)
|
||||
|
||||
|
||||
@router.get("/{org_id}", response_model=OrgResponse)
|
||||
@@ -76,12 +84,12 @@ async def get_organization(
|
||||
async def update_organization(
|
||||
org_id: int,
|
||||
data: OrgUpdate,
|
||||
request: Request,
|
||||
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:
|
||||
before = await row_to_dict(db, "organizations", org_id)
|
||||
if not before:
|
||||
raise HTTPException(status_code=404, detail="Organisation nicht gefunden")
|
||||
|
||||
updates = {}
|
||||
@@ -97,6 +105,13 @@ async def update_organization(
|
||||
await db.execute(f"UPDATE organizations SET {set_clause} WHERE id = ?", values)
|
||||
await db.commit()
|
||||
|
||||
after = await row_to_dict(db, "organizations", org_id)
|
||||
await log_action(
|
||||
db, admin, get_client_ip(request),
|
||||
action="update", resource_type="organization", resource_id=org_id,
|
||||
before=before, after=after,
|
||||
)
|
||||
|
||||
cursor = await db.execute("SELECT * FROM organizations WHERE id = ?", (org_id,))
|
||||
return await _enrich_org(db, await cursor.fetchone())
|
||||
|
||||
@@ -104,13 +119,18 @@ async def update_organization(
|
||||
@router.delete("/{org_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_organization(
|
||||
org_id: int,
|
||||
request: Request,
|
||||
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():
|
||||
before = await row_to_dict(db, "organizations", org_id)
|
||||
if not before:
|
||||
raise HTTPException(status_code=404, detail="Organisation nicht gefunden")
|
||||
|
||||
# Kaskadierendes Loeschen
|
||||
await db.execute("DELETE FROM organizations WHERE id = ?", (org_id,))
|
||||
await db.commit()
|
||||
await log_action(
|
||||
db, admin, get_client_ip(request),
|
||||
action="delete", resource_type="organization", resource_id=org_id,
|
||||
before=before,
|
||||
)
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren