diff --git a/src/main.py b/src/main.py index 4d69710..c4d7450 100644 --- a/src/main.py +++ b/src/main.py @@ -10,7 +10,7 @@ from config import STATIC_DIR, PORT from database import db_dependency from auth import verify_password, create_token from models import LoginRequest, TokenResponse -from routers import organizations, licenses, users, dashboard +from routers import organizations, licenses, users, dashboard, sources import aiosqlite @@ -39,6 +39,7 @@ app.include_router(organizations.router) app.include_router(licenses.router) app.include_router(users.router) app.include_router(dashboard.router) +app.include_router(sources.router) # --- Login --- diff --git a/src/routers/sources.py b/src/routers/sources.py new file mode 100644 index 0000000..047dc2b --- /dev/null +++ b/src/routers/sources.py @@ -0,0 +1,167 @@ +"""Grundquellen-Verwaltung und Kundenquellen-Übersicht.""" +from fastapi import APIRouter, Depends, HTTPException, status +from pydantic import BaseModel, Field +from typing import Optional +from auth import get_current_admin +from database import db_dependency +import aiosqlite + +router = APIRouter(prefix="/api/sources", tags=["sources"]) + + +class GlobalSourceCreate(BaseModel): + name: str = Field(min_length=1, max_length=200) + url: Optional[str] = None + domain: Optional[str] = None + source_type: str = Field(default="rss_feed", pattern="^(rss_feed|web_source|excluded)$") + category: str = Field(default="sonstige") + status: str = Field(default="active", pattern="^(active|inactive)$") + notes: Optional[str] = None + + +class GlobalSourceUpdate(BaseModel): + name: Optional[str] = Field(default=None, max_length=200) + url: Optional[str] = None + domain: Optional[str] = None + source_type: Optional[str] = Field(default=None, pattern="^(rss_feed|web_source|excluded)$") + category: Optional[str] = None + status: Optional[str] = Field(default=None, pattern="^(active|inactive)$") + notes: Optional[str] = None + + +@router.get("/global") +async def list_global_sources( + admin: dict = Depends(get_current_admin), + db: aiosqlite.Connection = Depends(db_dependency), +): + """Alle Grundquellen auflisten (tenant_id IS NULL).""" + cursor = await db.execute( + "SELECT * FROM sources WHERE tenant_id IS NULL ORDER BY category, source_type, name" + ) + return [dict(row) for row in await cursor.fetchall()] + + +@router.post("/global", status_code=201) +async def create_global_source( + data: GlobalSourceCreate, + admin: dict = Depends(get_current_admin), + db: aiosqlite.Connection = Depends(db_dependency), +): + """Neue Grundquelle anlegen.""" + if data.url: + cursor = await db.execute( + "SELECT id, name FROM sources WHERE url = ? AND tenant_id IS NULL", + (data.url,), + ) + existing = await cursor.fetchone() + if existing: + raise HTTPException( + status_code=409, + detail=f"URL bereits vorhanden: {existing['name']}", + ) + + cursor = await db.execute( + """INSERT INTO sources (name, url, domain, source_type, category, status, notes, added_by, tenant_id) + VALUES (?, ?, ?, ?, ?, ?, ?, 'system', NULL)""", + (data.name, data.url, data.domain, data.source_type, data.category, data.status, data.notes), + ) + await db.commit() + + cursor = await db.execute("SELECT * FROM sources WHERE id = ?", (cursor.lastrowid,)) + return dict(await cursor.fetchone()) + + +@router.put("/global/{source_id}") +async def update_global_source( + source_id: int, + data: GlobalSourceUpdate, + admin: dict = Depends(get_current_admin), + db: aiosqlite.Connection = Depends(db_dependency), +): + """Grundquelle bearbeiten.""" + cursor = await db.execute( + "SELECT * FROM sources WHERE id = ? AND tenant_id IS NULL", (source_id,) + ) + row = await cursor.fetchone() + if not row: + raise HTTPException(status_code=404, detail="Grundquelle nicht gefunden") + + updates = {} + for field, value in data.model_dump(exclude_none=True).items(): + updates[field] = value + + if not updates: + return dict(row) + + set_clause = ", ".join(f"{k} = ?" for k in updates) + values = list(updates.values()) + [source_id] + await db.execute(f"UPDATE sources SET {set_clause} WHERE id = ?", values) + await db.commit() + + cursor = await db.execute("SELECT * FROM sources WHERE id = ?", (source_id,)) + return dict(await cursor.fetchone()) + + +@router.delete("/global/{source_id}", status_code=204) +async def delete_global_source( + source_id: int, + admin: dict = Depends(get_current_admin), + db: aiosqlite.Connection = Depends(db_dependency), +): + """Grundquelle loeschen.""" + cursor = await db.execute( + "SELECT id FROM sources WHERE id = ? AND tenant_id IS NULL", (source_id,) + ) + if not await cursor.fetchone(): + raise HTTPException(status_code=404, detail="Grundquelle nicht gefunden") + + await db.execute("DELETE FROM sources WHERE id = ?", (source_id,)) + await db.commit() + + +@router.get("/tenant") +async def list_tenant_sources( + admin: dict = Depends(get_current_admin), + db: aiosqlite.Connection = Depends(db_dependency), +): + """Alle tenant-spezifischen Quellen mit Org-Name auflisten.""" + cursor = await db.execute(""" + SELECT s.*, o.name as org_name + FROM sources s + LEFT JOIN organizations o ON o.id = s.tenant_id + WHERE s.tenant_id IS NOT NULL + ORDER BY o.name, s.category, s.name + """) + return [dict(row) for row in await cursor.fetchall()] + + +@router.post("/tenant/{source_id}/promote") +async def promote_to_global( + source_id: int, + admin: dict = Depends(get_current_admin), + db: aiosqlite.Connection = Depends(db_dependency), +): + """Tenant-Quelle zur Grundquelle befoerdern.""" + cursor = await db.execute("SELECT * FROM sources WHERE id = ?", (source_id,)) + row = await cursor.fetchone() + if not row: + raise HTTPException(status_code=404, detail="Quelle nicht gefunden") + if row["tenant_id"] is None: + raise HTTPException(status_code=400, detail="Bereits eine Grundquelle") + + if row["url"]: + cursor = await db.execute( + "SELECT id FROM sources WHERE url = ? AND tenant_id IS NULL", + (row["url"],), + ) + if await cursor.fetchone(): + raise HTTPException(status_code=409, detail="URL bereits als Grundquelle vorhanden") + + await db.execute( + "UPDATE sources SET tenant_id = NULL, added_by = 'system' WHERE id = ?", + (source_id,), + ) + await db.commit() + + cursor = await db.execute("SELECT * FROM sources WHERE id = ?", (source_id,)) + return dict(await cursor.fetchone()) diff --git a/src/static/dashboard.html b/src/static/dashboard.html index 4f585a1..a792cc0 100644 --- a/src/static/dashboard.html +++ b/src/static/dashboard.html @@ -22,6 +22,7 @@ + @@ -193,6 +194,72 @@ + + +
+ + + +
+
+
+ + +
+ +
+
+
+ + + + + + + + + + + + + +
NameURLDomainTypKategorieStatusAktionen
+
+
+
+ + +
+
+
+ + +
+
+
+
+ + + + + + + + + + + + + +
NameDomainTypKategorieOrganisationHinzugefuegt vonAktionen
+
+
+
+
+ + + + +