"""Nutzer-Verwaltung pro Organisation.""" import secrets import string from datetime import datetime, timedelta, timezone from fastapi import APIRouter, Depends, HTTPException, status, Request from models import UserCreate, UserResponse from auth import get_current_admin from database import db_dependency from audit import log_action, get_client_ip, row_to_dict from config import MAGIC_LINK_BASE_URL 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, request: Request, 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() 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") 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']})") 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") username = data.username if data.username else email.split("@")[0] 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, username, org_id, data.role, now), ) user_id = cursor.lastrowid 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() 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(username, org["name"], code, link) await send_email(email, subject, html) except Exception: pass cursor = await db.execute("SELECT * FROM users WHERE id = ?", (user_id,)) new_user = dict(await cursor.fetchone()) await log_action( db, admin, get_client_ip(request), action="create", resource_type="user", resource_id=user_id, after={k: v for k, v in new_user.items() if k != "password_hash"}, ) return new_user async def _toggle_field(db, request, admin, user_id: int, field: str, value: int): """Hilfsfunktion: ein Feld aktualisieren + Audit.""" before = await row_to_dict(db, "users", user_id) if not before: raise HTTPException(status_code=404, detail="Nutzer nicht gefunden") await db.execute(f"UPDATE users SET {field} = ? WHERE id = ?", (value, user_id)) await db.commit() after = await row_to_dict(db, "users", user_id) await log_action( db, admin, get_client_ip(request), action="update", resource_type="user", resource_id=user_id, before={field: before.get(field)}, after={field: after.get(field)}, ) return after @router.put("/{user_id}/deactivate") async def deactivate_user( user_id: int, request: Request, admin: dict = Depends(get_current_admin), db: aiosqlite.Connection = Depends(db_dependency), ): await _toggle_field(db, request, admin, user_id, "is_active", 0) return {"ok": True} @router.put("/{user_id}/activate") async def activate_user( user_id: int, request: Request, admin: dict = Depends(get_current_admin), db: aiosqlite.Connection = Depends(db_dependency), ): await _toggle_field(db, request, admin, user_id, "is_active", 1) return {"ok": True} @router.put("/{user_id}/globe-access") async def toggle_globe_access( user_id: int, request: Request, admin: dict = Depends(get_current_admin), db: aiosqlite.Connection = Depends(db_dependency), ): cursor = await db.execute("SELECT id, globe_access FROM users WHERE id = ?", (user_id,)) row = await cursor.fetchone() if not row: raise HTTPException(status_code=404, detail="Nutzer nicht gefunden") new_val = 0 if row[1] else 1 await _toggle_field(db, request, admin, user_id, "globe_access", new_val) return {"ok": True, "globe_access": bool(new_val)} @router.put("/{user_id}/network-access") async def toggle_network_access( user_id: int, request: Request, admin: dict = Depends(get_current_admin), db: aiosqlite.Connection = Depends(db_dependency), ): cursor = await db.execute("SELECT id, network_access FROM users WHERE id = ?", (user_id,)) row = await cursor.fetchone() if not row: raise HTTPException(status_code=404, detail="Nutzer nicht gefunden") new_val = 0 if row[1] else 1 await _toggle_field(db, request, admin, user_id, "network_access", new_val) return {"ok": True, "network_access": bool(new_val)} @router.put("/{user_id}/role") async def change_role( user_id: int, request: Request, 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") await _toggle_field(db, request, admin, user_id, "role", role) return {"ok": True} @router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_user( user_id: int, request: Request, admin: dict = Depends(get_current_admin), db: aiosqlite.Connection = Depends(db_dependency), ): before = await row_to_dict(db, "users", user_id) if not before: raise HTTPException(status_code=404, detail="Nutzer nicht gefunden") await db.execute("DELETE FROM users WHERE id = ?", (user_id,)) await db.commit() await log_action( db, admin, get_client_ip(request), action="delete", resource_type="user", resource_id=user_id, before={k: v for k, v in before.items() if k != "password_hash"}, )