Projekt-Sichtbarkeit: Nutzer-Zuordnung, projektbezogene Filterung und Schreibschutz

- Neues Modell ProjektUser (projekt_users) für projektbezogene Sichtbarkeit
- Migration V01_09 legt Tabelle an (keine Initial-Befüllung)
- CRUD-Logik für Projekt-User-Zuordnungen mit Cross-DB-User-Auflösung
- Serverseitige Sichtbarkeits-Filterung: Projekt-Liste/Get filtert nach
  projekt_users-Zuordnung für Nicht-SysAdmin/Nicht-AppAdmin
- Auto-Assignment: Ersteller wird bei Projektanlage automatisch zugeordnet
- Sichtbarkeits-Filterung auf CallLog und Kontaktadresse erweitert
  (Liste und Einzelabruf)
- Schreibschutz: Alle Schreib-Endpunkte (update/delete) für Projekt,
  Kontaktadresse und CallLog prüfen Projekt-Zuordnung vor der Ausführung
- Hilfsfunktionen _get_visible_user_id() und _require_projekt_access()
  zur Kapselung der wiederkehrenden Prüfungen
- REST-Endpunkte /xhr/projektuser/{list,add,remove};
  Schreib-Endpunkte mit @authorisation_required([]) als Erweiterungspunkt
- Client: Neues Modell projekt_user.ts, Listen-Komponente mit Modal
  (Select-Widget), Button in Projekt-Maske, Route registriert
- Build aktualisiert (tsc + rollup)
- AGENTS.md: Vorgaben für projektbezogene Sichtbarkeit dokumentiert

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
gg committed 2026-09-05 15:45:32 +02:00
1 parent b2f8684afb
commit 4cb0084066
12 files changed
+758 -25

No files matched your search

+47 -3
View File
@@ -10,31 +10,54 @@ dort über den Decorator authorisation_required.
:version: 1.0
"""
from db import dbapp
from crm.model import Projekt, Kunde
from crm.model import Projekt, Kunde, ProjektUser
from auth.model.errors import ApplicationError, HtmlStatusCode
from flask_login import current_user
current_user: object
session = dbapp.session
def get_projekte(kunden_id: int | None = None):
def get_projekte(kunden_id: int | None = None, user_id: int | None = None):
"""
Liefert Projekte, sortiert nach projektname.
Ist kunden_id gesetzt, werden nur die Projekte dieses Kunden
zurückgegeben (serverseitige Filterung). Ohne kunden_id werden
alle Projekte geliefert.
Ist user_id gesetzt, werden nur Projekte geliefert, denen der
Nutzer über die Tabelle 'projekt_users' zugeordnet ist. Dies
steuert die Sichtbarkeit: Nicht zugeordnete Nutzer sehen diese
Projekte nicht. SysAdmin und AppAdmin übergeben user_id=None
und sehen alle Projekte.
"""
query = Projekt.query
if kunden_id is not None:
query = query.filter(Projekt.kunden_id == kunden_id)
if user_id is not None:
# Nur Projekte, denen der Nutzer zugeordnet ist. Der Subquery
# auf projekt_users filtert die sichtbaren Projekt-IDs.
query = query.filter(
Projekt.id.in_(
dbapp.session.query(ProjektUser.projekt_id).filter(
ProjektUser.user_id == user_id
)
)
)
return query.order_by(Projekt.projektname).all()
def get_projekt(projekt_id: int) -> Projekt:
def get_projekt(projekt_id: int, user_id: int | None = None) -> Projekt:
"""
Liefert ein einzelnes Projekt anhand seiner ID.
Wirft ApplicationError (404), falls das Projekt nicht existiert.
Ist user_id gesetzt, wird zusätzlich geprüft, ob der Nutzer dem
Projekt zugeordnet ist. Ist er nicht zugeordnet, wird ein 404
geliefert, damit nicht zugeordnete Nutzer keine Rückschlüsse auf
die Existenz des Projekts ziehen können.
"""
projekt = Projekt.query.get(projekt_id)
if projekt is None:
@@ -42,6 +65,19 @@ def get_projekt(projekt_id: int) -> Projekt:
"Projekt mit der ID {} wurde nicht gefunden.".format(projekt_id),
htmlstatus=HtmlStatusCode.Not_Found,
)
if user_id is not None:
# Sichtbarkeitsprüfung: Ist der Nutzer dem Projekt zugeordnet?
zuordnung = ProjektUser.query.filter(
ProjektUser.projekt_id == projekt_id,
ProjektUser.user_id == user_id,
).first()
if zuordnung is None:
# 404 statt 403, damit nicht zugeordnete Nutzer keine Info über
# die Existenz des Projekts erhalten.
raise ApplicationError(
"Projekt mit der ID {} wurde nicht gefunden.".format(projekt_id),
htmlstatus=HtmlStatusCode.Not_Found,
)
return projekt
@@ -78,6 +114,14 @@ def create_projekt(data: dict) -> Projekt:
projekt.startdatum = (data.get("startdatum") or "").strip() or None
session.add(projekt)
session.commit()
# Auto-Assignment: Den Ersteller dem neuen Projekt zuordnen, damit
# er es sehen kann. Nur für authentifizierte Nutzer mit einer id.
if hasattr(current_user, "id") and current_user.id is not None:
zuordnung = ProjektUser()
zuordnung.projekt_id = projekt.id
zuordnung.user_id = current_user.id
session.add(zuordnung)
session.commit()
return projekt