Per-User Domain-Ausschlüsse + Grundquellen-Schutz
- Neue Tabelle user_excluded_domains für benutzerspezifische Ausschlüsse - Domain-Ausschlüsse wirken nur für den jeweiligen User, nicht org-weit - user_id wird durch die gesamte Pipeline geschleust (Orchestrator → Researcher → RSS-Parser) - Grundquellen (is_global) können nicht mehr bearbeitet/gelöscht werden im Frontend - Grundquelle-Badge bei globalen Quellen statt Edit/Delete-Buttons - Filter Von mir ausgeschlossen im Quellen-Modal Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Dieser Commit ist enthalten in:
@@ -550,7 +550,7 @@ async def trigger_refresh(
|
||||
await _check_incident_access(db, incident_id, current_user["id"], tenant_id)
|
||||
|
||||
from agents.orchestrator import orchestrator
|
||||
enqueued = await orchestrator.enqueue_refresh(incident_id)
|
||||
enqueued = await orchestrator.enqueue_refresh(incident_id, user_id=current_user["id"])
|
||||
|
||||
if not enqueued:
|
||||
return {"status": "skipped", "incident_id": incident_id}
|
||||
|
||||
@@ -57,7 +57,12 @@ async def list_sources(
|
||||
query += " ORDER BY source_type, category, name"
|
||||
cursor = await db.execute(query, params)
|
||||
rows = await cursor.fetchall()
|
||||
return [dict(row) for row in rows]
|
||||
results = []
|
||||
for row in rows:
|
||||
d = dict(row)
|
||||
d["is_global"] = d.get("tenant_id") is None
|
||||
results.append(d)
|
||||
return results
|
||||
|
||||
|
||||
@router.get("/stats")
|
||||
@@ -285,64 +290,54 @@ async def rediscover_existing_endpoint(
|
||||
raise HTTPException(status_code=500, detail="Rediscovery fehlgeschlagen")
|
||||
|
||||
|
||||
@router.get("/my-exclusions")
|
||||
async def get_my_exclusions(
|
||||
current_user: dict = Depends(get_current_user),
|
||||
db: aiosqlite.Connection = Depends(db_dependency),
|
||||
):
|
||||
"""Gibt die vom aktuellen User ausgeschlossenen Domains zurück."""
|
||||
user_id = current_user["id"]
|
||||
cursor = await db.execute(
|
||||
"SELECT domain, notes, created_at FROM user_excluded_domains WHERE user_id = ? ORDER BY domain",
|
||||
(user_id,),
|
||||
)
|
||||
rows = await cursor.fetchall()
|
||||
return [dict(row) for row in rows]
|
||||
|
||||
|
||||
@router.post("/block-domain")
|
||||
async def block_domain(
|
||||
data: DomainActionRequest,
|
||||
current_user: dict = Depends(get_current_user),
|
||||
db: aiosqlite.Connection = Depends(db_dependency),
|
||||
):
|
||||
"""Domain ausschließen: Alle Feeds deaktivieren + excluded-Eintrag anlegen."""
|
||||
tenant_id = current_user.get("tenant_id")
|
||||
"""Domain fuer den aktuellen User ausschließen (per-User, nicht org-weit)."""
|
||||
user_id = current_user["id"]
|
||||
domain = data.domain.lower().strip()
|
||||
username = current_user["username"]
|
||||
|
||||
# Pruefen ob bereits ausgeschlossen
|
||||
cursor = await db.execute(
|
||||
"SELECT added_by FROM sources WHERE LOWER(domain) = ? AND source_type != 'excluded' AND status = 'active' AND (tenant_id IS NULL OR tenant_id = ?)",
|
||||
(domain, tenant_id),
|
||||
)
|
||||
affected = await cursor.fetchall()
|
||||
for row in affected:
|
||||
ab = row["added_by"] or ""
|
||||
if ab != "system" and ab != username and ab != "":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Domain enthaelt Quellen anderer Nutzer",
|
||||
)
|
||||
|
||||
cursor = await db.execute(
|
||||
"UPDATE sources SET status = 'inactive' WHERE LOWER(domain) = ? AND source_type != 'excluded' AND tenant_id = ?",
|
||||
(domain, tenant_id),
|
||||
)
|
||||
feeds_deactivated = cursor.rowcount
|
||||
|
||||
cursor = await db.execute(
|
||||
"SELECT id FROM sources WHERE LOWER(domain) = ? AND source_type = 'excluded' AND (tenant_id IS NULL OR tenant_id = ?)",
|
||||
(domain, tenant_id),
|
||||
"SELECT id FROM user_excluded_domains WHERE user_id = ? AND domain = ?",
|
||||
(user_id, domain),
|
||||
)
|
||||
existing = await cursor.fetchone()
|
||||
|
||||
if existing:
|
||||
excluded_id = existing["id"]
|
||||
if data.notes:
|
||||
await db.execute(
|
||||
"UPDATE sources SET notes = ? WHERE id = ?",
|
||||
(data.notes, excluded_id),
|
||||
"UPDATE user_excluded_domains SET notes = ? WHERE id = ?",
|
||||
(data.notes, existing["id"]),
|
||||
)
|
||||
else:
|
||||
cursor = await db.execute(
|
||||
"""INSERT INTO sources (name, url, domain, source_type, category, status, notes, added_by, tenant_id)
|
||||
VALUES (?, NULL, ?, 'excluded', 'sonstige', 'active', ?, ?, ?)""",
|
||||
(domain, domain, data.notes, current_user["username"], tenant_id),
|
||||
)
|
||||
excluded_id = cursor.lastrowid
|
||||
await db.commit()
|
||||
return {"domain": domain, "status": "already_excluded"}
|
||||
|
||||
await db.execute(
|
||||
"INSERT INTO user_excluded_domains (user_id, domain, notes) VALUES (?, ?, ?)",
|
||||
(user_id, domain, data.notes),
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
return {
|
||||
"domain": domain,
|
||||
"feeds_deactivated": feeds_deactivated,
|
||||
"excluded_id": excluded_id,
|
||||
}
|
||||
return {"domain": domain, "status": "excluded"}
|
||||
|
||||
|
||||
@router.post("/unblock-domain")
|
||||
@@ -351,41 +346,18 @@ async def unblock_domain(
|
||||
current_user: dict = Depends(get_current_user),
|
||||
db: aiosqlite.Connection = Depends(db_dependency),
|
||||
):
|
||||
"""Domain-Ausschluss aufheben: excluded-Eintrag loeschen + Feeds reaktivieren."""
|
||||
tenant_id = current_user.get("tenant_id")
|
||||
"""Domain-Ausschluss fuer den aktuellen User aufheben."""
|
||||
user_id = current_user["id"]
|
||||
domain = data.domain.lower().strip()
|
||||
|
||||
cursor = await db.execute(
|
||||
"SELECT COUNT(*) as cnt FROM sources WHERE LOWER(domain) = ? AND source_type != 'excluded' AND (tenant_id IS NULL OR tenant_id = ?)",
|
||||
(domain, tenant_id),
|
||||
"DELETE FROM user_excluded_domains WHERE user_id = ? AND domain = ?",
|
||||
(user_id, domain),
|
||||
)
|
||||
row = await cursor.fetchone()
|
||||
has_feeds = row["cnt"] > 0
|
||||
|
||||
if has_feeds:
|
||||
await db.execute(
|
||||
"DELETE FROM sources WHERE LOWER(domain) = ? AND source_type = 'excluded' AND tenant_id = ?",
|
||||
(domain, tenant_id),
|
||||
)
|
||||
cursor = await db.execute(
|
||||
"UPDATE sources SET status = 'active' WHERE LOWER(domain) = ? AND source_type != 'excluded' AND tenant_id = ?",
|
||||
(domain, tenant_id),
|
||||
)
|
||||
feeds_reactivated = cursor.rowcount
|
||||
else:
|
||||
await db.execute(
|
||||
"""UPDATE sources SET source_type = 'web_source', status = 'active', notes = 'Ausschluss aufgehoben'
|
||||
WHERE LOWER(domain) = ? AND source_type = 'excluded' AND (tenant_id IS NULL OR tenant_id = ?)""",
|
||||
(domain, tenant_id),
|
||||
)
|
||||
feeds_reactivated = 0
|
||||
|
||||
removed = cursor.rowcount
|
||||
await db.commit()
|
||||
|
||||
return {
|
||||
"domain": domain,
|
||||
"feeds_reactivated": feeds_reactivated,
|
||||
}
|
||||
return {"domain": domain, "removed": removed > 0}
|
||||
|
||||
|
||||
@router.delete("/domain/{domain}")
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren