Initial commit: AegisSight-Monitor-Verwaltung

Dieser Commit ist enthalten in:
claude-dev
2026-03-04 17:53:19 +01:00
Commit e5a11d3549
19 geänderte Dateien mit 2421 neuen und 0 gelöschten Zeilen

163
src/routers/users.py Normale Datei
Datei anzeigen

@@ -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()