diff --git a/App_Server/root/approot.py b/App_Server/root/approot.py index e40516f..ea2ede9 100644 --- a/App_Server/root/approot.py +++ b/App_Server/root/approot.py @@ -1,10 +1,9 @@ -from flask import Flask +from flask import Flask, jsonify from flask_login import LoginManager from settings import Configuration from db import dbauth from db import dbapp from auth.model.checkdb import checkupdates as checkupdatesauth -# from model.checkdb import checkupdates as checkupdatesappdb from os import path login_manager = LoginManager() @@ -15,7 +14,7 @@ def init_app(): config: Configuration = Configuration("root/config.ini") _projektname = config.app_projectname - + app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:////usr/var/pythonappdb/{}.db".format(_projektname) app.config["SQLALCHEMY_BINDS"] = { "auth": "sqlite:////usr/var/pythonappdb/auth.db", @@ -23,23 +22,43 @@ def init_app(): } app.config["DEBUG"] = path.exists("root/debug") app.config["PROJEKTNAME"] = config.app_projectname + app.config["APPID"] = config.app_appid app.config["APPVERSION"] = config.clientapp_version app.config["SECRET_KEY"] = config.flask_secret # Initialize Plugins + # dbapp ist dasselbe Objekt wie dbauth (siehe db.py); beide teilen sich + # die SQLAlchemy-Registry. Daher genügt ein einziger init_app-Aufruf, der + # sowohl den auth- als auch den appdb-Bind initialisiert. login_manager.init_app(app) dbauth.init_app(app) - #dbapp.init_app(app) + + # Globale Fehlerbehandlung für ApplicationExceptionen. Die Route-internen + # try/except-Blöcke (auth) fangen weiterhin lokal ab; für alle nicht + # gefangenen Fälle (insbesondere crm) greift dieser Handler. + from auth.model.errors import ApplicationError, AutorisationError + + @app.errorhandler(ApplicationError) + def handle_application_error(ex): + return jsonify(ex.error), ex.htmlstatus + + @app.errorhandler(AutorisationError) + def handle_autorisation_error(ex): + return jsonify(ex.error), ex.htmlstatus with app.app_context(): # Include our Routes print("before auth.routes", flush=True) from auth import routes from routes import general + from crm import routes as crm_routes + from crm.model import checkdb as crm_checkdb checkupdatesauth() - # checkupdatesappdb() + crm_checkdb.checkupdates() - # from maintenance import init_cli + # Flask CLI-Befehle (init-db, reset-db) registrieren + from cli import register_cli_commands + register_cli_commands(app) return app diff --git a/App_Server/root/auth/decorators.py b/App_Server/root/auth/decorators.py new file mode 100644 index 0000000..e1121a1 --- /dev/null +++ b/App_Server/root/auth/decorators.py @@ -0,0 +1,97 @@ +""" +Decoratoren zur Authentifizierungs- und Autorisierungsprüfung. + +Der Decorator authorisation_required hebt die Rollenprüfung an die Route-Ebene, +analog zu flask_login.login_required. Authentifizierung (login_required) ist +eingebettet, sodass pro Endpunkt nur ein Decorator nötig ist. + +:author: Mistral Vibe +:date: 2026-09-02 +:version: 1.0 +""" +from functools import wraps + +from flask import current_app +from flask_login import current_user, login_required + +from auth.model import APP_ADMIN +from auth.model.errors import AutorisationError + + +def authorisation_required(allowed_roles=[]): + """ + Decorator, der sicherstellt, dass der angemeldete Benutzer für den + Endpunkt autorisiert ist. Authentifizierung (login_required) ist eingebettet, + sodass kein zusätzlicher @login_required-Aufruf nötig ist. + + Aspekte der Autorisierung (Prüfreihenfolge): + + 1. SysAdmin (systemweite Super-Rolle): + Der SysAdmin darf alles, unabhängig von der Anwendung. Er braucht in + keiner Application eine Rolle zugewiesen zu haben. Die Prüfung erfolgt + über User.is_SysAdmin() (app-übergreifend). Dies ist die einzige Rolle, + die applikationsübergreifend gilt — sie ist eine Systemrolle, keine + App-Rolle. + + 2. AppAdmin (anwendungsbezogene Verwaltungsrolle): + Der AppAdmin darf innerhalb der Anwendung alles, der er zugeordnet ist. + Er braucht die Rolle AppAdmin nur in der aktuellen Anwendung. Der + Decorator ergänzt allowed_roles intern um APP_ADMIN, sodass der AppAdmin + jeden Endpunkt ohne explizite Nennung in allowed_roles passiert. Die + Prüfung erfolgt über User.has_permission(appid, [..., APP_ADMIN]) — + app-spezifisch, nicht systemweit. + + 3. Benutzer mit Basis-Rolle 'User' (Mitarbeiter): + Jeder Anwender hat zwingend die Basis-Rolle 'User' (Mitarbeiter), damit + er das Login-Gate in auth/routes.checklogin passiert. Diese Rolle allein + berechtigt nur zu Lese-Endpunkten, bei denen MITARBEITER in allowed_roles + steht. Für Schreib-Endpunkte reicht 'User' nicht. + + 4. Benutzer mit Zusatz-Rollen (z.B. KundenManager, später ProjektManager): + Zusätzlich zur Basis-Rolle 'User' können einem Benutzer weitere Rollen + zugewiesen werden. Diese gewähren weitergehende Berechtigungen für + bestimmte Entitäten. Die Prüfung erfolgt app-spezifisch über + User.has_permission(appid, allowed_roles). Der Benutzer muss die + Zusatz-Rolle in der aktuellen Anwendung haben, nicht in einer anderen. + + Die appid der aktuellen Anwendung wird aus app.config["APPID"] gelesen + (gesetzt in approot.py aus config.ini, Eintrag [APP] appid). Sie ist der + lesbare Anwendungsname (z.B. 'KBPlusCRM') und dient der Zuordnung in der + Tabelle 'applications'. Sie ist nicht identisch mit PROJEKTNAME, der nur + für Datei- und Verzeichnisnamen verwendet wird. + + - allowed_roles: List[str] + - roleids (nicht ids) der Rollen, die den Endpunkt zusätzlich zu + AppAdmin/SysAdmin erlauben. AppAdmin wird intern ergänzt, muss also + nicht explizit genannt werden. Der Benutzer muss die Rolle in der + aktuellen Anwendung haben (geprüft via User.has_permission). + """ + + def decorator(f): + @wraps(f) + @login_required + def wrapper(*args, **kwargs): + # appid der aktuellen Anwendung aus der Konfiguration + appid = current_app.config["APPID"] + + # 1. SysAdmin darf systemweit alles (keine app-spezifische Prüfung) + if current_user.is_SysAdmin(): + return f(*args, **kwargs) + + # 2.-4. AppAdmin und alle anderen Rollen: app-spezifische Prüfung. + # AppAdmin wird implizit zu allowed_roles hinzugefügt, damit der + # AppAdmin in seiner Anwendung jeden Endpunkt ohne explizite + # Nennung in allowed_roles nutzen kann. + effective_roles = list(allowed_roles) + [APP_ADMIN] + + if not current_user.has_permission(appid, effective_roles): + # Rollen des Users in der aktuellen Anwendung für die Fehlermeldung + user_roles = current_user.get_userapproles(appid) + raise AutorisationError( + allowed_roles=allowed_roles, user_roles=user_roles + ) + return f(*args, **kwargs) + + return wrapper + + return decorator diff --git a/App_Server/root/auth/model/checkdb.py b/App_Server/root/auth/model/checkdb.py index 869767e..3da50b0 100644 --- a/App_Server/root/auth/model/checkdb.py +++ b/App_Server/root/auth/model/checkdb.py @@ -3,6 +3,7 @@ from auth import model from sqlalchemy import text from sqlalchemy.exc import OperationalError from datetime import datetime +from flask import current_app from db import dbauth session = dbauth.session @@ -48,8 +49,11 @@ def _db_V01_01(): application = model.Application() application.id = 1 - application.appid = "Project01" - application.name = "Project01 (Name ändern)" + # appid und name aus der Konfiguration übernehmen (config.ini [APP] appid). + # Die appid ist der lesbare Anwendungsname, der zur Autorisierung verwendet + # wird; nicht identisch mit PROJEKTNAME (technischer Bezeichner). + application.appid = current_app.config["APPID"] + application.name = current_app.config["APPID"] session.add(application) userapp = model.Userapp() @@ -61,6 +65,23 @@ def _db_V01_01(): _updatesysteminfo('01.01') session.commit() +def _db_V01_02(): + """ + Migriert bestehende Application-Datensätze, deren appid noch den alten + Platzhalter 'Project01' enthält, auf die appid aus der aktuellen Konfiguration + (config.ini [APP] appid). Wird nach reset-db nicht benötigt (da _db_V01_01 + direkt den korrekten Wert setzt), greift aber bei Upgrades bestehender DBs. + """ + appid = current_app.config["APPID"] + application: model.Application = model.Application.query.filter( + model.Application.appid == "Project01" + ).first() + if application is not None: + application.appid = appid + application.name = appid + _updatesysteminfo('01.02') + session.commit() + def _updatesysteminfo(versionnr): s: model.Systeminfo = model.Systeminfo.query.get(1) s.version = versionnr diff --git a/App_Server/root/cli.py b/App_Server/root/cli.py new file mode 100644 index 0000000..469c240 --- /dev/null +++ b/App_Server/root/cli.py @@ -0,0 +1,127 @@ +""" +Flask CLI-Befehle zur Datenbank-Initialisierung und zum Reset. + +Befehle (aufgerufen via Flask CLI): + flask --app wsgi:app init-db -- Schema + Musterdaten anlegen + flask --app wsgi:app reset-db -- alles bis auf SysAdmin löschen + +init-db ist idempotent: bereits vorhandene Rollen/Kunden werden nicht +doppelt angelegt. reset-db fragt standardmäßig zur Bestätigung; mit --yes +wird die Abfrage übersprungen. + +:author: Mistral Vibe +:date: 2026-09-02 +:version: 1.0 +""" +import click +from flask import Flask + +from db import dbapp, dbauth + + +def register_cli_commands(app: Flask): + """Registriert die CLI-Befehle init-db und reset-db auf der Flask-App.""" + + @app.cli.command("init-db") + def init_db(): + """Initialisiert die Datenbanken und legt Musterdaten an.""" + click.echo("Initialisiere auth.db ...") + from auth.model import checkdb as auth_checkdb + auth_checkdb.checkupdates() + _seed_app_roles() + + click.echo("Initialisiere kbp_crm.db ...") + from crm.model import checkdb as crm_checkdb + crm_checkdb.checkupdates() + _seed_muster_kunden() + + click.echo("Fertig.") + + @app.cli.command("reset-db") + @click.option("--yes", is_flag=True, help="Bestätigungsabfrage überspringen") + def reset_db(yes): + """Setzt die Datenbanken zurück, behält den SysAdmin.""" + if not yes: + click.confirm( + "Wirklich alle Daten löschen? Der SysAdmin bleibt erhalten.", + abort=True, + ) + click.echo("Setze kbp_crm.db zurück ...") + _reset_appdb() + click.echo("Setze auth.db zurück (SysAdmin bleibt) ...") + _reset_authdb() + _seed_app_roles() + click.echo("Fertig.") + + +def _seed_app_roles(): + """ + Legt die App-Rollen Mitarbeiter und KundenManager idempotent an und ordnet + sie der ersten Application zu. AppAdmin existiert bereits (wird in + auth/model/checkdb._db_V01_01 angelegt). + """ + from auth.model import Application, Role + + session = dbauth.session + application = Application.query.first() + rollen = [ + ("User", "Mitarbeiter"), + ("KundenManager", "Kunden-Manager"), + ] + for roleid, rolename in rollen: + role = Role.query.filter(Role.roleid == roleid).first() + if role is None: + role = Role() + role.roleid = roleid + role.name = rolename + session.add(role) + if application is not None and role not in application.roles: + application.roles.append(role) + session.commit() + + +def _seed_muster_kunden(): + """ + Legt die drei Muster-Kunden an, falls die kunden-Tabelle noch leer ist. + """ + from crm.model import Kunde + + session = dbapp.session + if session.query(Kunde).count() > 0: + return + muster = [ + Kunde(name="Musterfirma GmbH", strasse="Musterstraße 1", + plz="12345", ort="Musterstadt", land="Deutschland"), + Kunde(name="Beispiel AG", strasse="Beispielweg 2", + plz="54321", ort="Beispielstadt", land="Deutschland"), + Kunde(name="Test Unternehmen", strasse="Testallee 3", + plz="67890", ort="Testdorf", land="Österreich"), + ] + for kunde in muster: + session.add(kunde) + session.commit() + + +def _reset_appdb(): + """ + Setzt die CRM-Datenbank zurück: alle Tabellen löschen und neu anlegen + (Schema + Systeminfo, keine Musterdaten). + """ + from crm.model import checkdb as crm_checkdb + + dbapp.drop_all(bind_key="appdb") + dbapp.session.remove() + crm_checkdb.checkupdates() + + +def _reset_authdb(): + """ + Setzt die Auth-Datenbank zurück: alle Tabellen löschen und neu anlegen. + Dadurch entsteht der frische Initialzustand mit SysAdmin-User, SysAdmin- und + AppAdmin-Rolle sowie der Application (entspricht auth/model/checkdb._db_V01_01). + """ + from auth.model import checkdb as auth_checkdb + + dbauth.drop_all(bind_key="auth") + dbauth.session.remove() + auth_checkdb.checkupdates() diff --git a/App_Server/root/config.ini b/App_Server/root/config.ini index bd44b9a..172760d 100644 --- a/App_Server/root/config.ini +++ b/App_Server/root/config.ini @@ -3,5 +3,6 @@ debug=True [APP] projectname=kbp_crm +appid=KBPlusCRM version=00.00.01 flasksecret=Hier_muss_ein_sicherer_schluessel_eingesetzt_werden \ No newline at end of file diff --git a/App_Server/root/crm/__init__.py b/App_Server/root/crm/__init__.py new file mode 100644 index 0000000..7ec6548 --- /dev/null +++ b/App_Server/root/crm/__init__.py @@ -0,0 +1,11 @@ +""" +CRM-Domainpaket. + +Enthält die serverseitige Logik und REST-Endpunkte für die CRM-Entitäten +(Kunde, Projekt, Kontakt, Kontaktadresse, Gesprächsnotiz). Die Datenmodelle +liegen in der separaten SQLite-Datenbank kbp_crm.db (Bind-Key 'appdb'). + +:author: Mistral Vibe +:date: 2026-09-02 +:version: 1.0 +""" diff --git a/App_Server/root/crm/model/__init__.py b/App_Server/root/crm/model/__init__.py new file mode 100644 index 0000000..d972eee --- /dev/null +++ b/App_Server/root/crm/model/__init__.py @@ -0,0 +1,96 @@ +""" +SQLAlchemy-Datenmodelle für die CRM-Domain. + +Alle Modelle verwenden den Bind-Key 'appdb' und liegen damit in der separaten +SQLite-Datenbank kbp_crm.db. Die Trennung zur Auth-Datenbank (Bind-Key 'auth') +erfolgt ausschließlich über __bind_key__. + +:author: Mistral Vibe +:date: 2026-09-02 +:version: 1.0 +""" +import datetime +import decimal + +from sqlalchemy import Column, DateTime, Integer, String + +from db import dbapp + + +# Bind-Key für alle CRM-Modelle (entspricht dem Eintrag in app.config["SQLALCHEMY_BINDS"]) +BINDKEY = 'appdb' + +# App-Rollen (roleids, konsistent zu den Konstanten in auth.model). Die Rolle +# 'AppAdmin' ist bereits in auth.model definiert und wird von den CRM-Routes +# implizit über den Decorator berücksichtigt. +# +# Rollenmodell (additiv): Jeder User erhält zwingend die Basis-Rolle 'User' +# (MITARBEITER, entspricht APP_USER in auth.model). Damit passiert er das +# Login-Gate in auth/routes.checklogin. Zusätzliche Rollen wie 'KundenManager' +# werden ergänzend zugewiesen und gewähren weitergehende Berechtigungen, die +# über den Decorator authorisation_required geprüft werden. +MITARBEITER = "User" +KUNDEN_MANAGER = "KundenManager" + + +def columns2dict(record, excludefields=[]): + """ + Wandelt die Spalten eines SQLAlchemy-Records in ein Dictionary um. + + Datums- und Decimal-Werte werden dabei in JSON-taugliche Strings konvertiert. + Über excludefields können Spalten ausgeschlossen werden (z.B. Passwort-Hashes). + """ + if not isinstance(excludefields, list): + excludefields = [] + attribs = dict() + if hasattr(record, '__table__'): + if hasattr(record.__table__, 'columns'): + for col in record.__table__.columns: + if col.name in excludefields: + continue + value = getattr(record, col.name) + if isinstance(value, datetime.date): + value = value.isoformat() + if isinstance(value, decimal.Decimal): + value = "{0:.2f}".format(value) + attribs[col.name] = value + return attribs + + +class CrmSysteminfo(dbapp.Model): + """ + Systeminformationstabelle für die CRM-Datenbank. + + Hält die DB-Versionsnummer, mit der checkdb feststellt, welche Migrationen + noch ausgeführt werden müssen. Der Klassenname unterscheidet sich bewusst + von auth.model.Systeminfo, da beide Klassen dieselbe SQLAlchemy-Registry + teilen und sich daher nicht namentlich doppeln dürfen. + """ + __tablename__ = 'systeminfo' + __bind_key__ = BINDKEY + id = Column(Integer, primary_key=True) + version = Column(String(20), nullable=False) + lastupdate = Column(DateTime, nullable=True) + initialinstall = Column(DateTime, nullable=True) + + +class Kunde(dbapp.Model): + """ + Kundenstamm: Verwaltung aller Kunden (Firmen oder Privatpersonen) mit den + relevanten Adressdaten. + """ + __tablename__ = 'kunden' + __bind_key__ = BINDKEY + id = Column(Integer, primary_key=True) + name = Column(String(200), nullable=False) + strasse = Column(String(200), nullable=True) + plz = Column(String(20), nullable=True) + ort = Column(String(100), nullable=True) + land = Column(String(80), nullable=True) + + def to_dict(self): + """Liefert die Kundendaten als Dictionary (für JSON-Serialisierung).""" + return columns2dict(self) + + +print("########## crm/model importiert") diff --git a/App_Server/root/crm/model/checkdb.py b/App_Server/root/crm/model/checkdb.py new file mode 100644 index 0000000..0cd8c01 --- /dev/null +++ b/App_Server/root/crm/model/checkdb.py @@ -0,0 +1,65 @@ +""" +DB-Versionierung für die CRM-Datenbank (kbp_crm.db). + +Analog zu auth/model/checkdb.py: Beim Start wird checkupdates() aufgerufen und +führt alle _db_Vxx_xx-Funktionen aus, deren Versionsnummer größer ist als die +in der Systeminfo-Tabelle vermerkte. Neue Migrationen werden als weitere +_db_Vxx_xx-Funktionen ergänzt. + +:author: Mistral Vibe +:date: 2026-09-02 +:version: 1.0 +""" +from datetime import datetime + +from sqlalchemy.exc import OperationalError + +from db import dbapp +from crm.model import CrmSysteminfo + +BINDKEY = "appdb" +session = dbapp.session + + +def _updatesysteminfo(versionnr: str): + """Aktualisiert Versionsnummer und Zeitstempel in der Systeminfo-Tabelle.""" + s: CrmSysteminfo = CrmSysteminfo.query.get(1) + s.version = versionnr + s.lastupdate = datetime.now() + + +def _db_V01_00(): + """ + Ersteinrichtung: Alle CRM-Tabellen anlegen und Systeminfo-Datensatz + schreiben. + """ + dbapp.create_all(bind_key=BINDKEY) + systeminfo = CrmSysteminfo() + systeminfo.id = 1 + systeminfo.version = "01.00" + systeminfo.lastupdate = datetime.now() + systeminfo.initialinstall = datetime.now() + session.add(systeminfo) + session.commit() + + +def checkupdates(): + """ + Ermittelt die aktuelle DB-Version aus der Systeminfo-Tabelle und führt alle + _db_Vxx_xx-Funktionen aus, deren Versionsnummer größer ist. Ist die + Datenbank noch leer (Tabelle fehlt), wird die Ersteinrichtung ausgeführt. + """ + curr_dbversion = '_db_V01_00' + try: + systeminfo: CrmSysteminfo = CrmSysteminfo.query.get(1) + curr_dbversion = "_db_V" + systeminfo.version.replace(".", "_") + except (OperationalError, AttributeError): + _db_V01_00() + + versionen = sorted(filter(lambda f: f.startswith("_db_V"), globals())) + + for v in versionen: + if v > curr_dbversion: + globals()[v]() + + print('########## crm Database updated') diff --git a/App_Server/root/crm/model/kunde.py b/App_Server/root/crm/model/kunde.py new file mode 100644 index 0000000..c9452aa --- /dev/null +++ b/App_Server/root/crm/model/kunde.py @@ -0,0 +1,91 @@ +""" +Geschäftslogik für die Entität Kunde. + +Diese Funktionen kapseln den DB-Zugriff und die fachlichen Validierungen. +Aufgerufen werden sie aus crm/routes.py; die Autorisierungsprüfung erfolgt +dort über den Decorator authorisation_required. + +:author: Mistral Vibe +:date: 2026-09-02 +:version: 1.0 +""" +from db import dbapp +from crm.model import Kunde + +from auth.model.errors import ApplicationError, HtmlStatusCode + +session = dbapp.session + + +def get_kunden(): + """ + Liefert alle Kunden, sortiert nach Name. + """ + return Kunde.query.order_by(Kunde.name).all() + + +def get_kunde(kunde_id: int) -> Kunde: + """ + Liefert einen einzelnen Kunden anhand seiner ID. + Wirft ApplicationError (404), falls der Kunde nicht existiert. + """ + kunde = Kunde.query.get(kunde_id) + if kunde is None: + raise ApplicationError( + "Kunde mit der ID {} wurde nicht gefunden.".format(kunde_id), + htmlstatus=HtmlStatusCode.Not_Found, + ) + return kunde + + +def create_kunde(data: dict) -> Kunde: + """ + Legt einen neuen Kunden an. Die vom Client übergebene ID wird ignoriert; + die ID wird serverseitig per Auto-Increment vergeben. + + - data: dict mit den Feldern name, strasse, plz, ort, land + """ + if not data.get("name") or not str(data["name"]).strip(): + raise ApplicationError( + "Der Kundenname darf nicht leer sein.", + htmlstatus=HtmlStatusCode.Bad_Request, + ) + kunde = Kunde() + kunde.name = data["name"].strip() + kunde.strasse = data.get("strasse") or None + kunde.plz = data.get("plz") or None + kunde.ort = data.get("ort") or None + kunde.land = data.get("land") or None + session.add(kunde) + session.commit() + return kunde + + +def update_kunde(data: dict) -> Kunde: + """ + Aktualisiert einen bestehenden Kunden. Die ID im data-Dict bestimmt den + zu ändernden Datensatz. + """ + kunde = get_kunde(data["id"]) + if not data.get("name") or not str(data["name"]).strip(): + raise ApplicationError( + "Der Kundenname darf nicht leer sein.", + htmlstatus=HtmlStatusCode.Bad_Request, + ) + kunde.name = data["name"].strip() + kunde.strasse = data.get("strasse") or None + kunde.plz = data.get("plz") or None + kunde.ort = data.get("ort") or None + kunde.land = data.get("land") or None + session.commit() + return kunde + + +def delete_kunde(kunde_id: int): + """ + Löscht einen Kunden anhand seiner ID. + Wirft ApplicationError (404), falls der Kunde nicht existiert. + """ + kunde = get_kunde(kunde_id) + session.delete(kunde) + session.commit() diff --git a/App_Server/root/crm/routes.py b/App_Server/root/crm/routes.py new file mode 100644 index 0000000..1873879 --- /dev/null +++ b/App_Server/root/crm/routes.py @@ -0,0 +1,71 @@ +""" +REST-Endpunkte für die CRM-Domain. + +Die Endpunkte orientieren sich 1:1 an den clientseitig definierten Pfaden +(siehe Client/models/*.ts). Pro Entität gibt es einen zusammenhängenden Block +mit den Aktionen list/get/create/update/delete. + +Die Autorisierung erfolgt über den Decorator authorisation_required; AppAdmin +und SysAdmin sind darin implizit immer erlaubt. Die Rollen Mitarbeiter und +KundenManager werden aus crm.model importiert. + +:author: Mistral Vibe +:date: 2026-09-02 +:version: 1.0 +""" +from flask import current_app as app +from flask import jsonify, request + +from auth.decorators import authorisation_required +from crm.model import KUNDEN_MANAGER, MITARBEITER +from crm.model import kunde as kunden + +print("crm-routes") + + +# --------------------------------------------------------------------------- # +# Kunde # +# --------------------------------------------------------------------------- # + + +@app.get("/xhr/kunde/list") +@authorisation_required([MITARBEITER, KUNDEN_MANAGER]) +def kunde_list(): + """Liefert alle Kunden als Liste.""" + result = [k.to_dict() for k in kunden.get_kunden()] + return jsonify(result), 200 + + +@app.get("/xhr/kunde/get") +@authorisation_required([MITARBEITER, KUNDEN_MANAGER]) +def kunde_get(): + """Liefert einen einzelnen Kunden anhand seiner ID (Query-Param id).""" + kunde = kunden.get_kunde(int(request.args.get("id"))) + return jsonify(kunde.to_dict()), 200 + + +@app.post("/xhr/kunde/create") +@authorisation_required([KUNDEN_MANAGER]) +def kunde_create(): + """Legt einen neuen Kunden an. ID wird serverseitig vergeben.""" + kunde = kunden.create_kunde(request.json) + return jsonify(kunde.to_dict()), 200 + + +@app.put("/xhr/kunde/update") +@authorisation_required([KUNDEN_MANAGER]) +def kunde_update(): + """Aktualisiert einen bestehenden Kunden.""" + kunde = kunden.update_kunde(request.json) + return jsonify(kunde.to_dict()), 200 + + +@app.delete("/xhr/kunde/delete") +@authorisation_required([KUNDEN_MANAGER]) +def kunde_delete(): + """Löscht einen Kunden anhand seiner ID (Query-Param id).""" + kunden.delete_kunde(int(request.args.get("id"))) + return jsonify({}), 200 + + +print("########## crm.routes importiert") diff --git a/App_Server/root/settings.py b/App_Server/root/settings.py index cba5e8b..84b1837 100644 --- a/App_Server/root/settings.py +++ b/App_Server/root/settings.py @@ -18,6 +18,10 @@ class Configuration: def app_projectname(self) -> str: return self.config["APP"].get("projectname") + @property + def app_appid(self) -> str: + return self.config["APP"].get("appid") + @property def flask_secret(self) -> str: return self.config["APP"].get("flasksecret") diff --git a/App_Server/root/wsgi.py b/App_Server/root/wsgi.py new file mode 100644 index 0000000..08b4690 --- /dev/null +++ b/App_Server/root/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI-Einstiegspunkt. + +Initialisiert die Flask-App auf Modulebene, sodass sie sowohl für produktive +WSGI-Server (Waitress) als auch für die Flask CLI erreichbar ist: + + flask --app wsgi:app init-db + flask --app wsgi:app reset-db + +:author: Mistral Vibe +:date: 2026-09-02 +:version: 1.0 +""" +from approot import init_app + +app = init_app()