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:
1 parent
b2f8684afb
commit
4cb0084066
12 files changed
+758
-25
No files matched your search
+127
-12
@@ -15,18 +15,65 @@ KundenManager werden aus crm.model importiert.
|
||||
"""
|
||||
from flask import current_app as app
|
||||
from flask import jsonify, request
|
||||
from flask_login import current_user
|
||||
|
||||
from auth.decorators import authorisation_required
|
||||
from auth.model import APP_ADMIN
|
||||
from auth.model.errors import ApplicationError, HtmlStatusCode
|
||||
from crm.model import KUNDEN_MANAGER, MITARBEITER
|
||||
from crm.model import kunde as kunden
|
||||
from crm.model import kontakt as kontakte
|
||||
from crm.model import projekt as projekte
|
||||
from crm.model import projekt_user as projekt_users
|
||||
from crm.model import kontaktadresse as kontaktadressen
|
||||
from crm.model import call_log as call_logs
|
||||
|
||||
print("crm-routes")
|
||||
|
||||
|
||||
def _get_visible_user_id():
|
||||
"""
|
||||
Liefert die user_id für die Sichtbarkeits-Filterung.
|
||||
|
||||
SysAdmin und AppAdmin sehen alle Projekte und abhängige Entitäten —
|
||||
für sie wird None zurückgegeben, sodass die Modell-Funktionen keinen
|
||||
Filter anwenden. Alle anderen Nutzer (Mitarbeiter, KundenManager)
|
||||
erhalten ihre current_user.id, sodass nur die ihnen zugeordneten
|
||||
Projekte bzw. abhängige Einträge geliefert werden.
|
||||
"""
|
||||
if current_user.is_SysAdmin() or current_user.has_permission(
|
||||
app.config["APPID"], [APP_ADMIN]
|
||||
):
|
||||
return None
|
||||
return current_user.id
|
||||
|
||||
|
||||
def _require_projekt_access(projekt_id: int):
|
||||
"""
|
||||
Prüft, ob der aktuelle Nutzer für das angegebene Projekt schreibberechtigt ist.
|
||||
|
||||
SysAdmin und AppAdmin haben immer Zugriff. Alle anderen Nutzer müssen
|
||||
dem Projekt über projekt_users zugeordnet sein. Ist der Nutzer nicht
|
||||
zugeordnet, wird ein ApplicationError (404) geworfen — 404 statt 403,
|
||||
damit nicht zugeordnete Nutzer keine Rückschlüsse auf die Existenz
|
||||
des Projekts ziehen können.
|
||||
|
||||
Muss in allen Schreib-Endpunkten (create/update/delete) aufgerufen
|
||||
werden, die ein konkretes Projekt betreffen, bevor die Änderung
|
||||
durchgeführt wird.
|
||||
|
||||
- projekt_id: int
|
||||
- Die ID des Projekts, auf das zugegriffen werden soll.
|
||||
"""
|
||||
user_id = _get_visible_user_id()
|
||||
if user_id is not None:
|
||||
if not projekt_users.is_user_assigned(projekt_id, user_id):
|
||||
raise ApplicationError(
|
||||
"Projekt mit der ID {} wurde nicht gefunden.".format(projekt_id),
|
||||
htmlstatus=HtmlStatusCode.Not_Found,
|
||||
)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Kunde #
|
||||
# --------------------------------------------------------------------------- #
|
||||
@@ -126,17 +173,23 @@ def kontakt_delete():
|
||||
@app.get("/xhr/projekt/list")
|
||||
@authorisation_required([MITARBEITER, KUNDEN_MANAGER])
|
||||
def projekt_list():
|
||||
"""Liefert Projekte als Liste. Optional nach kunden_id gefiltert."""
|
||||
"""Liefert Projekte als Liste. Optional nach kunden_id gefiltert.
|
||||
Für Nicht-SysAdmin/Nicht-AppAdmin werden nur die Projekte geliefert,
|
||||
denen der Nutzer über projekt_users zugeordnet ist."""
|
||||
kunden_id = request.args.get("kunden_id", type=int)
|
||||
result = [p.to_dict() for p in projekte.get_projekte(kunden_id)]
|
||||
user_id = _get_visible_user_id()
|
||||
result = [p.to_dict() for p in projekte.get_projekte(kunden_id, user_id)]
|
||||
return jsonify(result), 200
|
||||
|
||||
|
||||
@app.get("/xhr/projekt/get")
|
||||
@authorisation_required([MITARBEITER, KUNDEN_MANAGER])
|
||||
def projekt_get():
|
||||
"""Liefert ein einzelnes Projekt anhand seiner ID (Query-Param id)."""
|
||||
projekt = projekte.get_projekt(int(request.args.get("id")))
|
||||
"""Liefert ein einzelnes Projekt anhand seiner ID (Query-Param id).
|
||||
Für Nicht-SysAdmin/Nicht-AppAdmin wird geprüft, ob der Nutzer dem
|
||||
Projekt zugeordnet ist; sonst wird 404 geliefert."""
|
||||
user_id = _get_visible_user_id()
|
||||
projekt = projekte.get_projekt(int(request.args.get("id")), user_id)
|
||||
return jsonify(projekt.to_dict()), 200
|
||||
|
||||
|
||||
@@ -152,6 +205,7 @@ def projekt_create():
|
||||
@authorisation_required([KUNDEN_MANAGER])
|
||||
def projekt_update():
|
||||
"""Aktualisiert ein bestehendes Projekt."""
|
||||
_require_projekt_access(request.json["id"])
|
||||
projekt = projekte.update_projekt(request.json)
|
||||
return jsonify(projekt.to_dict()), 200
|
||||
|
||||
@@ -160,10 +214,49 @@ def projekt_update():
|
||||
@authorisation_required([KUNDEN_MANAGER])
|
||||
def projekt_delete():
|
||||
"""Löscht ein Projekt anhand seiner ID (Query-Param id)."""
|
||||
_require_projekt_access(int(request.args.get("id")))
|
||||
projekte.delete_projekt(int(request.args.get("id")))
|
||||
return jsonify({}), 200
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# ProjektUser (Nutzer-Zuordnung / Sichtbarkeit) #
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
@app.get("/xhr/projektuser/list")
|
||||
@authorisation_required([MITARBEITER, KUNDEN_MANAGER])
|
||||
def projektuser_list():
|
||||
"""Liefert alle einem Projekt zugeordneten Nutzer (Query-Param projekt_id)."""
|
||||
projekt_id = request.args.get("projekt_id", type=int)
|
||||
result = projekt_users.get_projekt_users(projekt_id)
|
||||
return jsonify(result), 200
|
||||
|
||||
|
||||
@app.post("/xhr/projektuser/add")
|
||||
# Leeres allowed_roles-Array als Erweiterungspunkt: Aktuell passieren nur
|
||||
# AppAdmin und SysAdmin (vom Decorator implizit ergänzt). Sobald später eine
|
||||
# weitere Rolle diese Endpunkte nutzen soll, wird sie einfach hier eingetragen,
|
||||
# z.B. @authorisation_required([KUNDEN_MANAGER]).
|
||||
@authorisation_required([])
|
||||
def projektuser_add():
|
||||
"""Ordnet einen Nutzer einem Projekt zu (body: {projekt_id, user_id})."""
|
||||
data = request.json
|
||||
projekt_users.add_projekt_user(data["projekt_id"], data["user_id"])
|
||||
return jsonify({}), 200
|
||||
|
||||
|
||||
@app.delete("/xhr/projektuser/remove")
|
||||
# Leeres allowed_roles-Array als Erweiterungspunkt (siehe projektuser_add).
|
||||
@authorisation_required([])
|
||||
def projektuser_remove():
|
||||
"""Entfernt die Zuordnung eines Nutzers zu einem Projekt (params: projekt_id, user_id)."""
|
||||
projekt_id = request.args.get("projekt_id", type=int)
|
||||
user_id = request.args.get("user_id", type=int)
|
||||
projekt_users.remove_projekt_user(projekt_id, user_id)
|
||||
return jsonify({}), 200
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Kontaktadresse #
|
||||
# --------------------------------------------------------------------------- #
|
||||
@@ -172,17 +265,22 @@ def projekt_delete():
|
||||
@app.get("/xhr/kontaktadresse/list")
|
||||
@authorisation_required([MITARBEITER, KUNDEN_MANAGER])
|
||||
def kontaktadresse_list():
|
||||
"""Liefert Kontaktadressen als Liste. Optional nach projekt_id gefiltert."""
|
||||
"""Liefert Kontaktadressen als Liste. Optional nach projekt_id gefiltert.
|
||||
Für Nicht-SysAdmin/Nicht-AppAdmin nur Kontaktadressen sichtbarer Projekte."""
|
||||
projekt_id = request.args.get("projekt_id", type=int)
|
||||
result = [k.to_dict() for k in kontaktadressen.get_kontaktadressen(projekt_id)]
|
||||
user_id = _get_visible_user_id()
|
||||
result = [k.to_dict() for k in kontaktadressen.get_kontaktadressen(projekt_id, user_id)]
|
||||
return jsonify(result), 200
|
||||
|
||||
|
||||
@app.get("/xhr/kontaktadresse/get")
|
||||
@authorisation_required([MITARBEITER, KUNDEN_MANAGER])
|
||||
def kontaktadresse_get():
|
||||
"""Liefert eine einzelne Kontaktadresse anhand ihrer ID (Query-Param id)."""
|
||||
kontaktadresse = kontaktadressen.get_kontaktadresse(int(request.args.get("id")))
|
||||
"""Liefert eine einzelne Kontaktadresse anhand ihrer ID (Query-Param id).
|
||||
Für Nicht-SysAdmin/Nicht-AppAdmin wird geprüft, ob der Nutzer dem
|
||||
Projekt der Kontaktadresse zugeordnet ist; sonst 404."""
|
||||
user_id = _get_visible_user_id()
|
||||
kontaktadresse = kontaktadressen.get_kontaktadresse(int(request.args.get("id")), user_id)
|
||||
return jsonify(kontaktadresse.to_dict()), 200
|
||||
|
||||
|
||||
@@ -190,6 +288,7 @@ def kontaktadresse_get():
|
||||
@authorisation_required([KUNDEN_MANAGER])
|
||||
def kontaktadresse_create():
|
||||
"""Legt eine neue Kontaktadresse an. ID wird serverseitig vergeben."""
|
||||
_require_projekt_access(int(request.json["projekt_id"]))
|
||||
kontaktadresse = kontaktadressen.create_kontaktadresse(request.json)
|
||||
return jsonify(kontaktadresse.to_dict()), 200
|
||||
|
||||
@@ -198,6 +297,7 @@ def kontaktadresse_create():
|
||||
@authorisation_required([KUNDEN_MANAGER])
|
||||
def kontaktadresse_update():
|
||||
"""Aktualisiert eine bestehende Kontaktadresse."""
|
||||
_require_projekt_access(int(request.json["projekt_id"]))
|
||||
kontaktadresse = kontaktadressen.update_kontaktadresse(request.json)
|
||||
return jsonify(kontaktadresse.to_dict()), 200
|
||||
|
||||
@@ -206,6 +306,10 @@ def kontaktadresse_update():
|
||||
@authorisation_required([KUNDEN_MANAGER])
|
||||
def kontaktadresse_delete():
|
||||
"""Löscht eine Kontaktadresse anhand ihrer ID (Query-Param id)."""
|
||||
# projekt_id der Kontaktadresse ermitteln, um die Projekt-Zuordnung
|
||||
# des Nutzers zu prüfen, bevor gelöscht wird.
|
||||
user_id = _get_visible_user_id()
|
||||
kontaktadresse = kontaktadressen.get_kontaktadresse(int(request.args.get("id")), user_id)
|
||||
kontaktadressen.delete_kontaktadresse(int(request.args.get("id")))
|
||||
return jsonify({}), 200
|
||||
|
||||
@@ -218,17 +322,22 @@ def kontaktadresse_delete():
|
||||
@app.get("/xhr/calllog/list")
|
||||
@authorisation_required([MITARBEITER, KUNDEN_MANAGER])
|
||||
def calllog_list():
|
||||
"""Liefert Gesprächsnotizen als Liste. Optional nach projekt_id gefiltert."""
|
||||
"""Liefert Gesprächsnotizen als Liste. Optional nach projekt_id gefiltert.
|
||||
Für Nicht-SysAdmin/Nicht-AppAdmin nur Notizen sichtbarer Projekte."""
|
||||
projekt_id = request.args.get("projekt_id", type=int)
|
||||
result = [l.to_dict() for l in call_logs.get_call_logs(projekt_id)]
|
||||
user_id = _get_visible_user_id()
|
||||
result = [l.to_dict() for l in call_logs.get_call_logs(projekt_id, user_id)]
|
||||
return jsonify(result), 200
|
||||
|
||||
|
||||
@app.get("/xhr/calllog/get")
|
||||
@authorisation_required([MITARBEITER, KUNDEN_MANAGER])
|
||||
def calllog_get():
|
||||
"""Liefert eine einzelne Gesprächsnotiz anhand ihrer ID (Query-Param id)."""
|
||||
call_log = call_logs.get_call_log(int(request.args.get("id")))
|
||||
"""Liefert eine einzelne Gesprächsnotiz anhand ihrer ID (Query-Param id).
|
||||
Für Nicht-SysAdmin/Nicht-AppAdmin wird geprüft, ob der Nutzer dem
|
||||
Projekt der Notiz zugeordnet ist; sonst 404."""
|
||||
user_id = _get_visible_user_id()
|
||||
call_log = call_logs.get_call_log(int(request.args.get("id")), user_id)
|
||||
return jsonify(call_log.to_dict()), 200
|
||||
|
||||
|
||||
@@ -236,6 +345,7 @@ def calllog_get():
|
||||
@authorisation_required([KUNDEN_MANAGER])
|
||||
def calllog_create():
|
||||
"""Legt eine neue Gesprächsnotiz an. ID wird serverseitig vergeben."""
|
||||
_require_projekt_access(int(request.json["projekt_id"]))
|
||||
call_log = call_logs.create_call_log(request.json)
|
||||
return jsonify(call_log.to_dict()), 200
|
||||
|
||||
@@ -244,6 +354,7 @@ def calllog_create():
|
||||
@authorisation_required([KUNDEN_MANAGER])
|
||||
def calllog_update():
|
||||
"""Aktualisiert eine bestehende Gesprächsnotiz."""
|
||||
_require_projekt_access(int(request.json["projekt_id"]))
|
||||
call_log = call_logs.update_call_log(request.json)
|
||||
return jsonify(call_log.to_dict()), 200
|
||||
|
||||
@@ -252,6 +363,10 @@ def calllog_update():
|
||||
@authorisation_required([KUNDEN_MANAGER])
|
||||
def calllog_delete():
|
||||
"""Löscht eine Gesprächsnotiz anhand ihrer ID (Query-Param id)."""
|
||||
# projekt_id der Notiz ermitteln, um die Projekt-Zuordnung
|
||||
# des Nutzers zu prüfen, bevor gelöscht wird.
|
||||
user_id = _get_visible_user_id()
|
||||
call_log = call_logs.get_call_log(int(request.args.get("id")), user_id)
|
||||
call_logs.delete_call_log(int(request.args.get("id")))
|
||||
return jsonify({}), 200
|
||||
|
||||
|
||||
Reference in new issue
Block a user