Compare commits

..
3 Commits
Author SHA1 Message Date
gg 13eaaec9c1 Server: CRM-Domain mit REST-API, Autorisierung und CLI
Neues crm-Paket mit Datenmodell, Geschäftslogik und REST-Endpunkten
für die Entität Kunde (/xhr/kunde/{list,get,create,update,delete}).

authorisation_required-Decorator: app-spezifische Rollenprüfung via
has_permission(appid, ...). SysAdmin systemweit, AppAdmin implizit in
seiner Anwendung, User (Mitarbeiter) als Basis-Rolle, Zusatz-Rollen
wie KundenManager additiv.

Flask CLI: init-db (Schema + Musterdaten + App-Rollen) und reset-db
(Bestätigung, behält SysAdmin).

config.ini: Neuer appid-Eintrag (KBPlusCRM), getrennt vom technischen
PROJEKTNAME. checkdb: appid aus config, Migration _db_V01_02 für
bestehende DBs.

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-09-03 16:12:13 +02:00
gg 65f81a0ee8 AGENTS.md: Vorgaben für git-Versionverwaltung und Server-Entwicklung ergänzt
Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-09-03 16:11:04 +02:00
gg f09828b208 index.html: Title auf KBPlus und Favicon aus static eingebunden
Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-09-02 14:48:34 +02:00
15 changed files with 633 additions and 9 deletions

No files matched your search

+5
View File
@@ -2,6 +2,9 @@
Beachte die strukturellen Vorgaben, die in ai-context/AI-operative-Vorgaben.md definiert sind.
In den Programmen sollen aussagekräftige Kommentare in deutsch erstellt werden.
## Vorgaben git Versionsverwaltung
Ein neuer Branch darf nur auf ausdrückliche Anweisung hin erstellt werden. Ohne diese Anwendung werden Änderungen immer in main gemacht.
## Vorgaben für die Typescript und mithriljs Entwicklung
Verwende bei der GUI primar die widgets, die es in client/Components/widgets gibt. Fehler widgets, gib eines Hinweis und erstelle einen Vorschlag für ein entsprechendes neues Widget.
Es soll immer ein responsive Design implmentiert werden, da die App sowohl auf PCs als auch auf mobilen Geräten laufen soll.
@@ -43,3 +46,5 @@ export const Select: m.ClosureComponent<iSelect> = () => {
}
}
```
## Vorgaben für die Server-seitige Entwicklung unter Python
+24 -5
View File
@@ -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()
@@ -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
+97
View File
@@ -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 <vibe@mistral.ai>
: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
+23 -2
View File
@@ -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
+127
View File
@@ -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 <vibe@mistral.ai>
: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()
+1
View File
@@ -3,5 +3,6 @@ debug=True
[APP]
projectname=kbp_crm
appid=KBPlusCRM
version=00.00.01
flasksecret=Hier_muss_ein_sicherer_schluessel_eingesetzt_werden
+11
View File
@@ -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 <vibe@mistral.ai>
:date: 2026-09-02
:version: 1.0
"""
+96
View File
@@ -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 <vibe@mistral.ai>
: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")
+65
View File
@@ -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 <vibe@mistral.ai>
: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')
+91
View File
@@ -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 <vibe@mistral.ai>
: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()
+71
View File
@@ -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 <vibe@mistral.ai>
: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")
+4
View File
@@ -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")
+2 -2
View File
@@ -5,7 +5,7 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>{{apptitle}}</title>
<title>KBPlus</title>
<meta name="description" content="A simple HTML5 Template for new projects.">
<meta name="author" content="SitePoint">
<meta name="clientappversion" content="{{clientappversion}}"/>
@@ -18,7 +18,7 @@
<meta property="og:image" content="image.png">
-->
<link rel="icon" href="/favicon.ico">
<link rel="icon" href="static/favicon.ico">
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
<link rel="stylesheet" id="apptheme" href="static/app-theme.css">
+16
View File
@@ -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 <vibe@mistral.ai>
:date: 2026-09-02
:version: 1.0
"""
from approot import init_app
app = init_app()
Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB