From 35247c895bac232e447e20f39dda0288726f78bb Mon Sep 17 00:00:00 2001 From: Tim Eertmoed Date: Fri, 11 Sep 2026 23:24:29 +0200 Subject: [PATCH] Version 2.7.2: Umlaute in der Oberflaeche, Einbindungen an der Sitzung --- apps/tesm-license/pyproject.toml | 2 +- .../tesm-license/src/tesm_license/__init__.py | 2 +- .../src/tesm_license/blueprints/api.py | 2 +- .../src/tesm_license/blueprints/backup.py | 22 +- .../src/tesm_license/blueprints/customers.py | 4 +- .../src/tesm_license/blueprints/licenses.py | 22 +- .../tesm_license/blueprints/own_license.py | 4 +- .../src/tesm_license/blueprints/portal.py | 10 +- .../tesm_license/blueprints/selfservice.py | 14 +- .../src/tesm_license/blueprints/settings.py | 14 +- .../src/tesm_license/blueprints/tickets.py | 14 +- apps/tesm-license/src/tesm_license/cli.py | 72 +++--- .../src/tesm_license/permissions.py | 20 +- .../src/tesm_license/services/activation.py | 18 +- .../src/tesm_license/services/issuance.py | 20 +- .../tesm_license/services/legacy_import.py | 48 ++-- .../src/tesm_license/services/mail.py | 4 +- .../src/tesm_license/services/master_key.py | 14 +- .../src/tesm_license/services/transfer.py | 4 +- .../src/tesm_license/settings_spec.py | 20 +- .../templates/license/license_detail.html | 4 +- .../templates/license/portal_request.html | 2 +- .../license/selfservice_offline.html | 4 +- .../templates/license/settings_vendor.html | 4 +- .../templates/license/ticket_detail.html | 4 +- .../templates/license/ticket_new.html | 8 +- deploy/install.sh | 9 + deploy/systemd/tesm-cleanup.service | 34 +++ deploy/systemd/tesm-cleanup.timer | 18 ++ deploy/systemd/tesm-license-cleanup.service | 34 +++ deploy/systemd/tesm-license-cleanup.timer | 18 ++ packages/tesm-core/pyproject.toml | 2 +- .../tesm-core/src/tesm_core/app_factory.py | 26 +- packages/tesm-core/src/tesm_core/audit.py | 4 +- .../tesm-core/src/tesm_core/auth/directory.py | 18 +- .../tesm-core/src/tesm_core/auth/models.py | 8 +- .../tesm-core/src/tesm_core/auth/service.py | 8 +- packages/tesm-core/src/tesm_core/cli_web.py | 24 +- packages/tesm-core/src/tesm_core/config.py | 4 +- packages/tesm-core/src/tesm_core/db.py | 27 ++- packages/tesm-core/src/tesm_core/extension.py | 2 +- packages/tesm-core/src/tesm_core/keystore.py | 12 +- .../src/tesm_core/licensing_client.py | 56 ++--- .../tesm-core/src/tesm_core/logging_setup.py | 2 +- .../tesm-core/src/tesm_core/migrations.py | 8 +- .../tesm-core/src/tesm_core/rbac/common.py | 24 +- .../tesm-core/src/tesm_core/rbac/model.py | 20 +- .../tesm-core/src/tesm_core/rbac/service.py | 40 +-- .../tesm-core/src/tesm_core/security/csrf.py | 2 +- .../src/tesm_core/security/passwords.py | 4 +- .../static/tesm_core/css/components.css | 80 +++++- .../src/tesm_core/static/tesm_core/js/app.js | 14 +- .../tesm_core/static/tesm_core/js/viewer.js | 227 ++++++++++++++---- packages/tesm-core/src/tesm_core/sysops.py | 12 +- .../tesm-core/src/tesm_core/system/hostnet.py | 14 +- .../src/tesm_core/system/webserver.py | 24 +- .../tesm_core/templates/tesm_core/base.html | 2 +- .../tesm_core/templates/tesm_core/users.html | 2 +- packages/tesm-core/src/tesm_core/transfer.py | 14 +- packages/tesm-core/src/tesm_core/ui.py | 2 +- .../tesm-core/src/tesm_core/views/account.py | 18 +- .../tesm-core/src/tesm_core/views/admin.py | 12 +- .../src/tesm_core/views/diagnostics.py | 10 +- .../src/tesm_core/views/hostsettings.py | 28 +-- .../tesm-core/src/tesm_core/views/logs.py | 2 +- .../tesm-core/src/tesm_core/views/trash.py | 22 +- packages/tesm-licensing-server/pyproject.toml | 4 +- .../src/tesm_licensing_server/__init__.py | 8 +- packages/tesm-licensing/pyproject.toml | 2 +- .../src/tesm_licensing/canonical.py | 6 +- .../src/tesm_licensing/license.py | 16 +- .../src/tesm_licensing/protocol.py | 16 +- 72 files changed, 825 insertions(+), 470 deletions(-) create mode 100644 deploy/systemd/tesm-cleanup.service create mode 100644 deploy/systemd/tesm-cleanup.timer create mode 100644 deploy/systemd/tesm-license-cleanup.service create mode 100644 deploy/systemd/tesm-license-cleanup.timer diff --git a/apps/tesm-license/pyproject.toml b/apps/tesm-license/pyproject.toml index bfe23c2..8a85b5f 100644 --- a/apps/tesm-license/pyproject.toml +++ b/apps/tesm-license/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "tesm-license" -version = "2.7.1" +version = "2.7.2" description = "TESM-Lizenzserver -- Kunden, Tickets, Lizenzen, Aktivierung" requires-python = ">=3.11" dependencies = [ diff --git a/apps/tesm-license/src/tesm_license/__init__.py b/apps/tesm-license/src/tesm_license/__init__.py index 5dd39a1..8ade82f 100644 --- a/apps/tesm-license/src/tesm_license/__init__.py +++ b/apps/tesm-license/src/tesm_license/__init__.py @@ -204,7 +204,7 @@ def _status_pills(user: Any) -> list[dict[str, str]]: def _startup_tasks(app: Flask) -> None: extension = core(app) if not extension.config.web_process: - logger.info("Startaufgaben uebersprungen -- kein Webprozess.") + logger.info("Startaufgaben übersprungen – kein Webprozess.") return from .services import activation diff --git a/apps/tesm-license/src/tesm_license/blueprints/api.py b/apps/tesm-license/src/tesm_license/blueprints/api.py index ad8975a..8242f74 100644 --- a/apps/tesm-license/src/tesm_license/blueprints/api.py +++ b/apps/tesm-license/src/tesm_license/blueprints/api.py @@ -59,7 +59,7 @@ def _handle(action: str): ) if request.content_length and request.content_length > MAX_BODY_BYTES: - return jsonify({"error": "too_large", "message": "Anfrage zu gross."}), 413 + return jsonify({"error": "too_large", "message": "Anfrage zu groß."}), 413 payload = request.get_json(silent=True) if not isinstance(payload, dict): diff --git a/apps/tesm-license/src/tesm_license/blueprints/backup.py b/apps/tesm-license/src/tesm_license/blueprints/backup.py index a131a66..045b844 100644 --- a/apps/tesm-license/src/tesm_license/blueprints/backup.py +++ b/apps/tesm-license/src/tesm_license/blueprints/backup.py @@ -63,12 +63,12 @@ def export(): allowed = {category.key for category in _allowed_categories()} chosen = [key for key in request.form.getlist("categories") if key in allowed] if not chosen: - flash("Es wurde keine Kategorie ausgewaehlt.", "warning") + flash("Es wurde keine Kategorie ausgewählt.", "warning") return redirect(url_for("backup.overview")) passphrase = request.form.get("passphrase") or "" if passphrase != (request.form.get("passphrase_repeat") or ""): - flash("Die beiden Passphrasen stimmen nicht ueberein.", "danger") + flash("Die beiden Passphrasen stimmen nicht überein.", "danger") return redirect(url_for("backup.overview")) try: @@ -101,11 +101,11 @@ def import_preview(): conn = db() upload = request.files.get("backup_file") if upload is None or not upload.filename: - flash("Keine Datei ausgewaehlt.", "danger") + flash("Keine Datei ausgewählt.", "danger") return redirect(url_for("backup.overview")) raw = upload.stream.read(MAX_IMPORT_BYTES + 1) if len(raw) > MAX_IMPORT_BYTES: - flash("Die Datei ist zu gross.", "danger") + flash("Die Datei ist zu groß.", "danger") return redirect(url_for("backup.overview")) try: @@ -124,8 +124,8 @@ def import_preview(): scope="sicherung", ) flash( - f"Datei geoeffnet. Sie haben {transfer.PREVIEW_TTL_MINUTES} Minuten Zeit, die " - "Uebernahme zu bestaetigen.", + f"Datei geöffnet. Sie haben {transfer.PREVIEW_TTL_MINUTES} Minuten Zeit, die " + "Übernahme zu bestätigen.", "success", ) return redirect(url_for("backup.overview")) @@ -139,13 +139,13 @@ def import_apply(): conn = db() token = session.get(PREVIEW_SESSION_KEY, "") if not token: - flash("Es liegt keine geoeffnete Sicherung vor.", "danger") + flash("Es liegt keine geöffnete Sicherung vor.", "danger") return redirect(url_for("backup.overview")) allowed = {category.key for category in _allowed_categories()} chosen = [key for key in request.form.getlist("categories") if key in allowed] if not chosen: - flash("Es wurde keine Kategorie ausgewaehlt.", "warning") + flash("Es wurde keine Kategorie ausgewählt.", "warning") return redirect(url_for("backup.overview")) try: @@ -172,7 +172,7 @@ def import_apply(): action="backup.imported", detail="; ".join( f"{key}: {counts['created']} neu, {counts['updated']} aktualisiert, " - f"{counts['skipped']} uebersprungen" + f"{counts['skipped']} übersprungen" for key, counts in report.items() ), ip=client_ip(), @@ -182,11 +182,11 @@ def import_apply(): lines = [ f"{transfer.CATEGORY_BY_KEY[key].label}: {counts['created']} neu, " f"{counts['updated']} aktualisiert" - + (f", {counts['skipped']} uebersprungen" if counts["skipped"] else "") + + (f", {counts['skipped']} übersprungen" if counts["skipped"] else "") for key, counts in report.items() if key in transfer.CATEGORY_BY_KEY ] - flash("Uebernahme abgeschlossen. " + " | ".join(lines), "success") + flash("Übernahme abgeschlossen. " + " – ".join(lines), "success") return redirect(url_for("backup.overview")) diff --git a/apps/tesm-license/src/tesm_license/blueprints/customers.py b/apps/tesm-license/src/tesm_license/blueprints/customers.py index a10e152..902affc 100644 --- a/apps/tesm-license/src/tesm_license/blueprints/customers.py +++ b/apps/tesm-license/src/tesm_license/blueprints/customers.py @@ -96,7 +96,7 @@ def update(customer_id: int): severity="notice", scope="kunde", ) - flash("Aenderungen gespeichert." if changes else "Keine Aenderungen.", + flash("Änderungen gespeichert." if changes else "Keine Änderungen.", "success" if changes else "info") except issuance.IssuanceError as exc: flash(str(exc), "danger") @@ -119,7 +119,7 @@ def delete(customer_id: int): severity="warning", scope="kunde", ) - flash("Kunde geloescht.", "success") + flash("Kunde gelöscht.", "success") return redirect(url_for("customers.overview")) except issuance.IssuanceError as exc: flash(str(exc), "danger") diff --git a/apps/tesm-license/src/tesm_license/blueprints/licenses.py b/apps/tesm-license/src/tesm_license/blueprints/licenses.py index c6ffd13..dec8850 100644 --- a/apps/tesm-license/src/tesm_license/blueprints/licenses.py +++ b/apps/tesm-license/src/tesm_license/blueprints/licenses.py @@ -124,7 +124,7 @@ def issue(ticket_id: str): if result.already_existed: flash( - "Zu diesem Ticket war bereits eine Lizenz offen -- sie wird angezeigt statt eine " + "Zu diesem Ticket war bereits eine Lizenz offen – sie wird angezeigt statt eine " "zweite auszustellen.", "info", ) @@ -166,7 +166,7 @@ def download(license_id: str): channel="download", recipient=current_user.username, actor=current_user.username, - detail="Vollstaendiges Bundle" if "client_key" in bundle else "Nur Lizenzdokument", + detail="Vollständiges Bundle" if "client_key" in bundle else "Nur Lizenzdokument", ) log_event("license.downloaded", target=license_id[:12], severity="notice") return Response( @@ -202,7 +202,7 @@ def resign(license_id: str): try: days = int(request.form["extend_days"]) except ValueError: - flash("Die Verlaengerung muss eine Zahl sein.", "danger") + flash("Die Verlängerung muss eine Zahl sein.", "danger") return redirect(url_for("licenses.detail", license_id=license_id)) if days > 0: document = json.loads(str(row["license_document"])) @@ -212,7 +212,7 @@ def resign(license_id: str): changes["expires_at"] = lic.iso(base + timedelta(days=days)) if not changes: - flash("Es wurde keine Aenderung angegeben.", "info") + flash("Es wurde keine Änderung angegeben.", "info") return redirect(url_for("licenses.detail", license_id=license_id)) key = signing_key() @@ -233,7 +233,7 @@ def resign(license_id: str): ) reporting.report_soon(core()) flash( - f"Neue Revision {updated['seq']} signiert. Der Client erhaelt sie beim naechsten " + f"Neue Revision {updated['seq']} signiert. Der Client erhält sie beim nächsten " "Heartbeat.", "success", ) @@ -258,7 +258,7 @@ def revoke(license_id: str): ) reporting.report_soon(core()) flash( - "Lizenz widerrufen. Das Ticket bleibt offen -- eine neue Lizenz kann sofort " + "Lizenz widerrufen. Das Ticket bleibt offen – eine neue Lizenz kann sofort " "ausgestellt werden.", "success", ) @@ -277,7 +277,7 @@ def delete(license_id: str): return redirect(url_for("licenses.detail", license_id=license_id)) log_event("license.deleted", target=license_id[:12], severity="warning") reporting.report_soon(core()) - flash("Lizenz geloescht.", "success") + flash("Lizenz gelöscht.", "success") return redirect(url_for("licenses.overview")) @@ -295,7 +295,7 @@ def send_mail(license_id: str): abort(404) recipient = (request.form.get("recipient") or str(row["contact_email"] or "")).strip() if not recipient: - flash("Es ist keine Empfaengeradresse hinterlegt.", "danger") + flash("Es ist keine Empfängeradresse hinterlegt.", "danger") return redirect(url_for("licenses.detail", license_id=license_id)) try: @@ -315,7 +315,7 @@ def send_mail(license_id: str): "", f"Lizenz-ID: {license_id}", f"Typ: {type_labels().get(str(row['type']), str(row['type']))}", - f"Gueltig bis: {row['expires_at']}" + f"Gültig bis: {row['expires_at']}" + (" (unbefristet)" if row["lifetime"] else ""), ] if is_customer and row["ticket_id"]: @@ -329,7 +329,7 @@ def send_mail(license_id: str): "Ihre Stammdaten pflegen:", customer_facing_url("portal.request_link"), ] - lines += ["", "Freundliche Gruesse", vendor.get("name") or "Ihr Lizenzgeber"] + lines += ["", "Freundliche Grüße", vendor.get("name") or "Ihr Lizenzgeber"] if vendor.get("phone"): lines.append(f"Telefon: {vendor['phone']}") if vendor.get("email"): @@ -403,7 +403,7 @@ def manual_code(): if outcome.ok: response_code = lic.encode_code(outcome.response) ok = True - message = "Antwortcode erzeugt. Bitte an den Kunden zuruecksenden." + message = "Antwortcode erzeugt. Bitte an den Kunden zurücksenden." log_event( f"license.offline_{payload.get('action')}", target=str(payload.get("license_id") or "")[:12], diff --git a/apps/tesm-license/src/tesm_license/blueprints/own_license.py b/apps/tesm-license/src/tesm_license/blueprints/own_license.py index 9da479d..461253e 100644 --- a/apps/tesm-license/src/tesm_license/blueprints/own_license.py +++ b/apps/tesm-license/src/tesm_license/blueprints/own_license.py @@ -79,11 +79,11 @@ def upload(): upload_file = request.files.get("license_file") if upload_file is None or not upload_file.filename: - flash("Keine Datei ausgewaehlt.", "danger") + flash("Keine Datei ausgewählt.", "danger") return redirect(url_for("own_license.overview")) raw = upload_file.stream.read(MAX_UPLOAD_BYTES + 1) if len(raw) > MAX_UPLOAD_BYTES: - flash("Die Datei ist zu gross fuer eine Lizenzdatei.", "danger") + flash("Die Datei ist zu groß für eine Lizenzdatei.", "danger") return redirect(url_for("own_license.overview")) try: diff --git a/apps/tesm-license/src/tesm_license/blueprints/portal.py b/apps/tesm-license/src/tesm_license/blueprints/portal.py index bfac568..cf4473d 100644 --- a/apps/tesm-license/src/tesm_license/blueprints/portal.py +++ b/apps/tesm-license/src/tesm_license/blueprints/portal.py @@ -45,7 +45,7 @@ RATE_WINDOW_MINUTES = 15 GENERIC_ANSWER = ( "Falls diese E-Mail-Adresse bei uns hinterlegt ist, wurde soeben eine Nachricht mit " - "einem {minutes} Minuten gueltigen Zugangslink verschickt." + "einem {minutes} Minuten gültigen Zugangslink verschickt." ) EDITABLE_FIELDS = ( @@ -150,13 +150,13 @@ def request_link(): f"Guten Tag{(' ' + str(customer['contact_person'])) if customer['contact_person'] else ''},", "", "mit dem folgenden Link erreichen Sie Ihre Daten im Kundenportal.", - f"Der Link ist {minutes} Minuten gueltig und laesst sich nur einmal verwenden:", + f"Der Link ist {minutes} Minuten gültig und lässt sich nur einmal verwenden:", "", access_url, "", "Wenn Sie diesen Zugang nicht angefragt haben, ignorieren Sie die Nachricht.", "", - "Freundliche Gruesse", + "Freundliche Grüße", vendor.get("name") or "Ihr Lizenzgeber", ] ), @@ -185,7 +185,7 @@ def access(token: str): expires = _parse(row["expires_at"]) if row is not None else None valid = row is not None and row["used_at"] is None and expires is not None and _now() < expires if not valid: - flash("Dieser Link ist ungueltig oder abgelaufen.", "danger") + flash("Dieser Link ist ungültig oder abgelaufen.", "danger") return redirect(url_for("portal.request_link")) minutes = int(extension.settings.get(conn, "portal_session_minutes") or 60) @@ -247,7 +247,7 @@ def me(): ip=client_ip(), severity="notice", ) - flash("Ihre Angaben wurden gespeichert." if changes else "Keine Aenderungen.", "success") + flash("Ihre Angaben wurden gespeichert." if changes else "Keine Änderungen.", "success") except issuance.IssuanceError as exc: flash(str(exc), "danger") return redirect(url_for("portal.me")) diff --git a/apps/tesm-license/src/tesm_license/blueprints/selfservice.py b/apps/tesm-license/src/tesm_license/blueprints/selfservice.py index ebcc34c..fb4e8ca 100644 --- a/apps/tesm-license/src/tesm_license/blueprints/selfservice.py +++ b/apps/tesm-license/src/tesm_license/blueprints/selfservice.py @@ -62,7 +62,7 @@ def _rate_guard() -> None: with transaction(): ratelimit.record(conn, RATE_BUCKET, client_ip() or "unbekannt") if verdict.blocked: - abort(429, description="Zu viele Anfragen. Bitte spaeter erneut versuchen.") + abort(429, description="Zu viele Anfragen. Bitte später erneut versuchen.") def _needs_offline(conn, ticket_id: str): # type: ignore[no-untyped-def] @@ -141,8 +141,8 @@ def issue(ticket_id: str): return redirect(url_for("selfservice.overview", ticket_id=ticket_id)) if issuance.ticket_has_open_license(conn, ticket_id): flash( - "Es existiert bereits eine gueltige Lizenz. Geben Sie diese zuerst frei, wenn Sie " - "auf ein anderes System wechseln moechten.", + "Es existiert bereits eine gültige Lizenz. Geben Sie diese zuerst frei, wenn Sie " + "auf ein anderes System wechseln möchten.", "warning", ) return redirect(url_for("selfservice.overview", ticket_id=ticket_id)) @@ -238,12 +238,12 @@ def offline(ticket_id: str): _log( "selfservice.offline_rejected", ticket, - "Code gehoert nicht zu diesem Vorgang", + "Code gehört nicht zu diesem Vorgang", severity="warning", ) flash( - "Dieser Code gehoert nicht zu Ihrem Vorgang. Bitte pruefen Sie, ob Sie ihn " - "vollstaendig kopiert haben.", + "Dieser Code gehört nicht zu Ihrem Vorgang. Bitte prüfen Sie, ob Sie ihn " + "vollständig kopiert haben.", "danger", ) return redirect(url_for("selfservice.offline", ticket_id=ticket_id)) @@ -299,7 +299,7 @@ def revoke(ticket_id: str, license_id: str): _log("license.revoked_self_service", ticket, license_id[:12], severity="warning") reporting.report_soon(core()) flash( - "Die Lizenz wurde freigegeben. Sie koennen jetzt eine neue Lizenz fuer Ihr neues System " + "Die Lizenz wurde freigegeben. Sie können jetzt eine neue Lizenz für Ihr neues System " "ausstellen.", "success", ) diff --git a/apps/tesm-license/src/tesm_license/blueprints/settings.py b/apps/tesm-license/src/tesm_license/blueprints/settings.py index de4b088..1f2f169 100644 --- a/apps/tesm-license/src/tesm_license/blueprints/settings.py +++ b/apps/tesm-license/src/tesm_license/blueprints/settings.py @@ -121,7 +121,7 @@ def system_save(): ip=client_ip(), severity="notice", ) - flash("Einstellungen gespeichert." if changed else "Keine Aenderungen.", + flash("Einstellungen gespeichert." if changed else "Keine Änderungen.", "success" if changed else "info") return redirect(url_for("settings.system")) @@ -133,10 +133,10 @@ def set_hostname(): sysops = extension.sysops hostname = (request.form.get("hostname") or "").strip() if not hostname or len(hostname) > 63 or not hostname.replace("-", "").isalnum(): - flash("Der Hostname ist ungueltig.", "danger") + flash("Der Hostname ist ungültig.", "danger") return redirect(url_for("settings.system")) if sysops is None or not sysops.available: - flash("Systemaktionen sind in dieser Umgebung nicht verfuegbar.", "warning") + flash("Systemaktionen sind in dieser Umgebung nicht verfügbar.", "warning") return redirect(url_for("settings.system")) result = sysops.set_hostname(hostname) if result.ok: @@ -158,7 +158,7 @@ def set_timezone(): flash("Unbekannte Zeitzone.", "danger") return redirect(url_for("settings.system")) if sysops is None or not sysops.available: - flash("Systemaktionen sind in dieser Umgebung nicht verfuegbar.", "warning") + flash("Systemaktionen sind in dieser Umgebung nicht verfügbar.", "warning") return redirect(url_for("settings.system")) result = sysops.set_timezone(timezone) if result.ok: @@ -227,7 +227,7 @@ def vendor_save(): if suffix not in LOGO_MIME: logo_problem = "Erlaubt sind PNG, JPG und SVG." elif len(data) > MAX_LOGO_BYTES: - logo_problem = f"Das Logo ist zu gross (maximal {MAX_LOGO_BYTES // 1024} KB)." + logo_problem = f"Das Logo ist zu groß (maximal {MAX_LOGO_BYTES // 1024} KB)." elif not any(head.startswith(magic) for magic in LOGO_MAGIC): logo_problem = "Die Datei ist kein Bild." else: @@ -245,7 +245,7 @@ def vendor_save(): log_event( "settings.vendor_saved", detail=", ".join(changed), severity="notice", scope="anbieter" ) - flash("Anbieterdaten gespeichert." if changed else "Keine Aenderungen.", + flash("Anbieterdaten gespeichert." if changed else "Keine Änderungen.", "success" if changed else "info") return redirect(url_for("settings.vendor")) @@ -297,7 +297,7 @@ def mail_save(): mail.store_secret(conn, request.form.get("client_secret")) changed.append("client_secret") log_event("settings.mail_saved", detail=", ".join(changed), severity="notice", scope="mail") - flash("E-Mail-Einstellungen gespeichert." if changed else "Keine Aenderungen.", + flash("E-Mail-Einstellungen gespeichert." if changed else "Keine Änderungen.", "success" if changed else "info") return redirect(url_for("settings.mail_settings")) diff --git a/apps/tesm-license/src/tesm_license/blueprints/tickets.py b/apps/tesm-license/src/tesm_license/blueprints/tickets.py index 85387bf..ef23f17 100644 --- a/apps/tesm-license/src/tesm_license/blueprints/tickets.py +++ b/apps/tesm-license/src/tesm_license/blueprints/tickets.py @@ -106,9 +106,9 @@ def _endpoint_missing() -> str: if master_endpoint(): return "" return ( - "Es ist kein oeffentlicher Endpunkt hinterlegt (Einstellungen -> Anbieter). " - "Solange er fehlt, koennen keine Lizenzen ausgestellt werden: der Endpunkt wird " - "in die Lizenz signiert und laesst sich spaeter nicht nachtragen." + "Es ist kein öffentlicher Endpunkt hinterlegt (Einstellungen -> Anbieter). " + "Solange er fehlt, können keine Lizenzen ausgestellt werden: der Endpunkt wird " + "in die Lizenz signiert und lässt sich später nicht nachtragen." ) @@ -176,9 +176,9 @@ def update(ticket_id: str): ) reporting.report_soon(core()) flash( - "Gespeichert. Die Aenderung wirkt auf die naechste Ausstellung." + "Gespeichert. Die Änderung wirkt auf die nächste Ausstellung." if changes - else "Keine Aenderungen.", + else "Keine Änderungen.", "success" if changes else "info", ) except (issuance.IssuanceError, ValueError) as exc: @@ -205,7 +205,7 @@ def toggle_block(ticket_id: str): ) reporting.report_soon(core()) flash( - "Ticket gesperrt -- es lassen sich keine neuen Lizenzen mehr ausstellen." + "Ticket gesperrt – es lassen sich keine neuen Lizenzen mehr ausstellen." if new_status == "blocked" else "Ticket entsperrt.", "success", @@ -231,7 +231,7 @@ def delete(ticket_id: str): scope="ticket", ) reporting.report_soon(core()) - flash("Ticket und seine Lizenzhistorie geloescht.", "success") + flash("Ticket und seine Lizenzhistorie gelöscht.", "success") return redirect(url_for("tickets.overview")) except issuance.IssuanceError as exc: flash(str(exc), "danger") diff --git a/apps/tesm-license/src/tesm_license/cli.py b/apps/tesm-license/src/tesm_license/cli.py index 2cb5cca..85ea113 100644 --- a/apps/tesm-license/src/tesm_license/cli.py +++ b/apps/tesm-license/src/tesm_license/cli.py @@ -42,22 +42,22 @@ def cmd_init_key(args: argparse.Namespace) -> int: path = master_key_path(extension.config) if path.exists() and not args.force: key = master_key.load(path) - print(f"Signaturschluessel existiert bereits: {key.key_id} ({path})") - print("Ein Austausch macht ALLE bereits ausgestellten Lizenzen unpruefbar.") + print(f"Signaturschlüssel existiert bereits: {key.key_id} ({path})") + print("Ein Austausch macht ALLE bereits ausgestellten Lizenzen unprüfbar.") return 0 if path.exists() and args.force: print( - "ABBRUCH: --force auf einen bestehenden Signaturschluessel wird nicht unterstuetzt.\n" - "Ein neuer Schluessel entwertet jede ausgestellte Lizenz. Bitte die Datei bewusst\n" + "ABBRUCH: --force auf einen bestehenden Signaturschlüssel wird nicht unterstützt.\n" + "Ein neuer Schlüssel entwertet jede ausgestellte Lizenz. Bitte die Datei bewusst\n" "von Hand beiseite legen, wenn das wirklich gewollt ist.", file=sys.stderr, ) return 2 key = master_key.ensure(path) - print(f"Signaturschluessel erzeugt: {key.key_id}") + print(f"Signaturschlüssel erzeugt: {key.key_id}") print(f"Datei: {path}") - print("Bitte SOFORT sichern -- ohne sie ist keine ausgestellte Lizenz mehr pruefbar.") - print(f"Oeffentlicher Schluessel (zum Pinnen beim Kunden):\n{key.public_key}") + print("Bitte SOFORT sichern – ohne sie ist keine ausgestellte Lizenz mehr prüfbar.") + print(f"Öffentlicher Schlüssel (zum Pinnen beim Kunden):\n{key.public_key}") return 0 @@ -65,11 +65,11 @@ def cmd_bootstrap(args: argparse.Namespace) -> int: """Stellt die eigene Lizenz dieser Wurzelinstanz aus -- nur in der Master-Ausgabe.""" if not is_master_edition(): for zeile in ( - "Diese Ausgabe ist fuer Sub-Lizenzserver und kann keine Master-Lizenz erzeugen.", + "Diese Ausgabe ist für Sub-Lizenzserver und kann keine Master-Lizenz erzeugen.", "", "Ein Sub-Lizenzserver bekommt seine Lizenz vom Master-Lizenzserver und spielt", "sie unter 'Eigene Lizenz' ein. Danach stellt er selbst Kundenlizenzen aus --", - "mit seinem eigenen Signaturschluessel, der beim Installieren entstanden ist.", + "mit seinem eigenen Signaturschlüssel, der beim Installieren entstanden ist.", "", "Die Master-Ausgabe liegt im Repository tesm-license-master.", ): @@ -83,10 +83,10 @@ def cmd_bootstrap(args: argparse.Namespace) -> int: app, extension, days=int(args.days), name=args.name ) print(f"Master-Lizenz ausgestellt: {document['license_id']}") - print(f"Gueltig bis: {document['expires_at']}") + print(f"Gültig bis: {document['expires_at']}") print(f"Datei: {extension.config.license_path}") if state is not None: - print(f"Zustand: gueltig={state.valid}, aktiviert={state.activated}, Rolle={state.role}") + print(f"Zustand: gültig={state.valid}, aktiviert={state.activated}, Rolle={state.role}") return 0 @@ -98,7 +98,7 @@ def cmd_create_admin(args: argparse.Namespace) -> int: return 2 password = args.password or getpass.getpass("Passwort: ") if not args.password and password != getpass.getpass("Passwort wiederholen: "): - print("Die Passwoerter stimmen nicht ueberein.", file=sys.stderr) + print("Die Passwörter stimmen nicht überein.", file=sys.stderr) return 2 problems = extension.password_policy.check(password, username=username) if problems and not args.force: @@ -180,7 +180,7 @@ def cmd_status(args: argparse.Namespace) -> int: + ("unversehrt" if chain["ok"] else f"GEBROCHEN bei {chain['broken_at']}") ) print( - "Signaturschluessel: " + "Signaturschlüssel: " + (str(key_health.get("key_id")) if key_health.get("present") else "FEHLT") ) for problem in key_health.get("problems", []): @@ -253,17 +253,17 @@ def cmd_migrate_legacy(args: argparse.Namespace) -> int: except legacy_import.LegacyImportError as exc: if args.adopt_key: raise - print(f"Hinweis: alter Signaturschluessel nicht lesbar ({exc}).") + print(f"Hinweis: alter Signaturschlüssel nicht lesbar ({exc}).") if key_path.is_file(): existing = master_key.load(key_path) report.key_id = existing.key_id if legacy_public and existing.public_key == legacy_public: - print(f"Signaturschluessel stimmt bereits ueberein ({existing.key_id}).") + print(f"Signaturschlüssel stimmt bereits überein ({existing.key_id}).") elif not legacy_public: issuer_changes = True print( - f"Es wird mit dem vorhandenen Signaturschluessel {existing.key_id} " + f"Es wird mit dem vorhandenen Signaturschlüssel {existing.key_id} " "gearbeitet." ) else: @@ -271,7 +271,7 @@ def cmd_migrate_legacy(args: argparse.Namespace) -> int: # ausgestellte Lizenz waere danach unpruefbar. issuer_changes = True print( - f"Diese Installation hat einen eigenen Signaturschluessel " + f"Diese Installation hat einen eigenen Signaturschlüssel " f"({existing.key_id}); der alte war {lic.key_id(legacy_public)}. " "Es wird mit dem vorhandenen weitergearbeitet." ) @@ -279,12 +279,12 @@ def cmd_migrate_legacy(args: argparse.Namespace) -> int: legacy_private, legacy_public = legacy_import.read_legacy_key(root) report.key_id = legacy_import.adopt_key(legacy_private, legacy_public, key_path) report.key_adopted = True - print(f"Signaturschluessel uebernommen: {report.key_id}") + print(f"Signaturschlüssel übernommen: {report.key_id}") else: print( - "Diese Installation hat noch keinen Signaturschluessel. Mit --adopt-key " - "wird der alte uebernommen -- dann bleibt die Anbieteridentitaet erhalten " - "und Kunden muessen keinen Ausstellerwechsel bestaetigen.", + "Diese Installation hat noch keinen Signaturschlüssel. Mit --adopt-key " + "wird der alte übernommen – dann bleibt die Anbieteridentität erhalten " + "und Kunden müssen keinen Ausstellerwechsel bestätigen.", file=sys.stderr, ) return 2 @@ -312,7 +312,7 @@ def cmd_migrate_legacy(args: argparse.Namespace) -> int: # Uebernahme. for einstellung, wert in uebernommen.items(): gekuerzt = wert if len(wert) <= 60 else wert[:57] + "..." - print(f"Uebernommen: {einstellung} = {gekuerzt}") + print(f"Übernommen: {einstellung} = {gekuerzt}") vendor = { "name": str(extension.settings.get(conn, "vendor_name") or "Eigenbetrieb"), @@ -326,7 +326,7 @@ def cmd_migrate_legacy(args: argparse.Namespace) -> int: print( "Es ist kein Endpunkt hinterlegt, und die alte Installation nennt " "auch keinen. Bitte unter Einstellungen -> Anbieter eintragen: ohne " - "ihn koennen uebernommene Lizenzen sich spaeter nicht aktivieren.", + "ihn können übernommene Lizenzen sich später nicht aktivieren.", file=sys.stderr, ) return 2 @@ -347,10 +347,10 @@ def cmd_migrate_legacy(args: argparse.Namespace) -> int: result.key_id = report.key_id if issuer_changes: result.warnings.append( - "Die Lizenzen sind mit einem anderen Schluessel signiert als " - "frueher. Kunden, die den alten Aussteller gepinnt haben, muessen " - "beim Einspielen einmal 'Wechsel des Ausstellers ausdruecklich " - "zulassen' bestaetigen." + "Die Lizenzen sind mit einem anderen Schlüssel signiert als " + "früher. Kunden, die den alten Aussteller gepinnt haben, müssen " + "beim Einspielen einmal 'Wechsel des Ausstellers ausdrücklich " + "zulassen' bestätigen." ) if args.dry_run: # Nichts festschreiben -- der Lauf war nur zur Ansicht. @@ -385,7 +385,7 @@ def build_parser() -> argparse.ArgumentParser: ) sub = parser.add_subparsers(dest="command", required=True) - init_key = sub.add_parser("init-key", help="Signaturschluessel erzeugen (einmalig)") + init_key = sub.add_parser("init-key", help="Signaturschlüssel erzeugen (einmalig)") init_key.add_argument("--force", action="store_true") init_key.set_defaults(func=cmd_init_key) @@ -401,7 +401,7 @@ def build_parser() -> argparse.ArgumentParser: bootstrap.add_argument("--name", default="Lizenzserver (Eigenbetrieb)") bootstrap.set_defaults(func=cmd_bootstrap) - admin = sub.add_parser("create-admin", help="Administratorkonto anlegen oder zuruecksetzen") + admin = sub.add_parser("create-admin", help="Administratorkonto anlegen oder zurücksetzen") admin.add_argument("--username") admin.add_argument("--password") admin.add_argument("--force", action="store_true") @@ -411,16 +411,16 @@ def build_parser() -> argparse.ArgumentParser: status.add_argument("--json", action="store_true") status.set_defaults(func=cmd_status) - house = sub.add_parser("housekeeping", help="Laufzeitdaten aufraeumen") + house = sub.add_parser("housekeeping", help="Laufzeitdaten aufräumen") house.set_defaults(func=cmd_housekeeping) migrate = sub.add_parser( "migrate-legacy", - help="Lizenzserver der Vorgaengerfassung uebernehmen", + help="Lizenzserver der Vorgängerfassung übernehmen", description=( - "Uebernimmt Signaturschluessel, Kunden, Tickets und Lizenzen aus einer alten " + "Übernimmt Signaturschlüssel, Kunden, Tickets und Lizenzen aus einer alten " "Installation. Die alte Installation wird nur gelesen. Jede Lizenz wird im " - "neuen Format neu signiert -- die Kunden brauchen danach eine neue " + "neuen Format neu signiert – die Kunden brauchen danach eine neue " "Lizenzdatei, weil die alte Signatur nicht mehr gilt." ), ) @@ -430,13 +430,13 @@ def build_parser() -> argparse.ArgumentParser: migrate.add_argument( "--adopt-key", action="store_true", - help="Den alten Signaturschluessel uebernehmen. Nur moeglich, solange diese " - "Installation noch keinen eigenen hat -- ein vorhandener wird nie ueberschrieben.", + help="Den alten Signaturschlüssel übernehmen. Nur möglich, solange diese " + "Installation noch keinen eigenen hat – ein vorhandener wird nie überschrieben.", ) migrate.add_argument( "--dry-run", action="store_true", - help="Nur anzeigen, was passieren wuerde -- nichts schreiben", + help="Nur anzeigen, was passieren würde – nichts schreiben", ) migrate.set_defaults(func=cmd_migrate_legacy) diff --git a/apps/tesm-license/src/tesm_license/permissions.py b/apps/tesm-license/src/tesm_license/permissions.py index 1a10cce..c774221 100644 --- a/apps/tesm-license/src/tesm_license/permissions.py +++ b/apps/tesm-license/src/tesm_license/permissions.py @@ -30,18 +30,18 @@ CUSTOMERS = Resource( label="Kunden", actions=(A.VIEW, A.CREATE, A.EDIT, A.DELETE), hint="Stammdaten der Kundeninstanzen", - action_hints={A.DELETE: "Nur moeglich, solange kein Ticket am Kunden haengt."}, + action_hints={A.DELETE: "Nur möglich, solange kein Ticket am Kunden hängt."}, ) TICKETS = Resource( key="tickets", label="Tickets", actions=(A.VIEW, A.CREATE, A.EDIT, A.DELETE), - hint="Dauerhafte Anspruechte auf eine Lizenz", + hint="Dauerhafte Ansprüche auf eine Lizenz", action_hints={ - A.EDIT: "Parameter aendern und Ticket sperren/entsperren. Wirkt erst auf die " - "naechste Ausstellung -- bereits ausgestellte Lizenzen bleiben unveraendert.", - A.DELETE: "Nur moeglich, wenn keine offene Lizenz mehr existiert.", + A.EDIT: "Parameter ändern und Ticket sperren/entsperren. Wirkt erst auf die " + "nächste Ausstellung – bereits ausgestellte Lizenzen bleiben unverändert.", + A.DELETE: "Nur möglich, wenn keine offene Lizenz mehr existiert.", }, ) @@ -62,7 +62,7 @@ PORTAL = Resource( key="portal", label="Kundenportal", actions=(A.VIEW, A.EXECUTE), - hint="Zugangslinks fuer Kunden", + hint="Zugangslinks für Kunden", action_hints={A.EXECUTE: "Zugangslink erzeugen und versenden."}, ) @@ -79,7 +79,7 @@ MAIL = Resource( actions=(A.VIEW, A.EDIT, A.EXECUTE, A.SECRETS), hint="Microsoft Graph (App-only)", action_hints={ - A.EXECUTE: "Verbindungstest ausfuehren.", + A.EXECUTE: "Verbindungstest ausführen.", A.SECRETS: "Client-Secret setzen.", }, ) @@ -88,7 +88,7 @@ VENDOR = Resource( key="settings_vendor", label="Anbieterdaten", actions=(A.VIEW, A.EDIT), - hint="Name, Kontakt, Logo und oeffentlicher Endpunkt -- reisen in jeder Lizenz mit", + hint="Name, Kontakt, Logo und öffentlicher Endpunkt – reisen in jeder Lizenz mit", ) LICENSING_AREA = Area( @@ -144,7 +144,7 @@ ROLE_PRESETS: tuple[RolePreset, ...] = ( RolePreset( key="auditor", name="Revision", - description="Nur lesen -- inklusive Aenderungsprotokoll und Diagnose.", + description="Nur lesen – inklusive Änderungsprotokoll und Diagnose.", grants=( ("customers", (A.VIEW,)), ("tickets", (A.VIEW,)), @@ -161,7 +161,7 @@ ROLE_PRESETS: tuple[RolePreset, ...] = ( RolePreset( key="administration", name="Administration", - description="Benutzer, Gruppen und Einstellungen -- ohne Admin-Kennzeichen.", + description="Benutzer, Gruppen und Einstellungen – ohne Admin-Kennzeichen.", grants=( ("users", "*"), ("groups", "*"), diff --git a/apps/tesm-license/src/tesm_license/services/activation.py b/apps/tesm-license/src/tesm_license/services/activation.py index 5aeb7fc..9b3f8d0 100644 --- a/apps/tesm-license/src/tesm_license/services/activation.py +++ b/apps/tesm-license/src/tesm_license/services/activation.py @@ -132,7 +132,7 @@ def _verify( seen_nonce=seen, ) except lic.SignatureInvalid: - return None, _error(400, "invalid_signature", "Signatur der Anfrage ist ungueltig.") + return None, _error(400, "invalid_signature", "Signatur der Anfrage ist ungültig.") except lic.ReplayDetected as exc: return None, _error(409, "replay", str(exc)) except lic.ProtocolViolation as exc: @@ -150,7 +150,7 @@ def process_activate( ) -> Outcome: row, error = _verify(conn, request, expected_action="activate") if error is not None or row is None: - return error or _error(400, "bad_request", "Ungueltige Anfrage.") + return error or _error(400, "bad_request", "Ungültige Anfrage.") fingerprint = str(request["fingerprint"]) hostname = str(request.get("hostname") or "") @@ -202,7 +202,7 @@ def process_activate( target=str(row["license_id"]), detail=( f"Fingerprint {fingerprint[:16]}..., Host {hostname or 'unbekannt'}" - + (f", verdraengt {len(superseded)} Lizenz(en)" if superseded else "") + + (f", verdrängt {len(superseded)} Lizenz(en)" if superseded else "") ), severity="notice", ) @@ -211,7 +211,7 @@ def process_activate( request, master_private_key=master_private_key, status="ok", - detail="Aktivierung bestaetigt.", + detail="Aktivierung bestätigt.", ) @@ -220,7 +220,7 @@ def process_deactivate( ) -> Outcome: row, error = _verify(conn, request, expected_action="deactivate") if error is not None or row is None: - return error or _error(400, "bad_request", "Ungueltige Anfrage.") + return error or _error(400, "bad_request", "Ungültige Anfrage.") fingerprint = str(request["fingerprint"]) if str(row["status"]) != "active" or str(row["fingerprint"] or "") != fingerprint: @@ -249,7 +249,7 @@ def process_deactivate( request, master_private_key=master_private_key, status="ok", - detail="Deaktivierung bestaetigt. Die Lizenz kann auf einer anderen Maschine aktiviert werden.", + detail="Deaktivierung bestätigt. Die Lizenz kann auf einer anderen Maschine aktiviert werden.", ) @@ -258,7 +258,7 @@ def process_heartbeat( ) -> Outcome: row, error = _verify(conn, request, expected_action="heartbeat") if error is not None or row is None: - return error or _error(400, "bad_request", "Ungueltige Anfrage.") + return error or _error(400, "bad_request", "Ungültige Anfrage.") fingerprint = str(request["fingerprint"]) hostname = str(request.get("hostname") or "") @@ -306,7 +306,7 @@ def process_heartbeat( ) else: logger.warning( - "Bericht von Lizenz %s verworfen -- Rolle ist %s, nicht license_server", + "Bericht von Lizenz %s verworfen | Rolle ist %s, nicht license_server", row["license_id"], row["role"], ) @@ -322,7 +322,7 @@ def process_heartbeat( request, master_private_key=master_private_key, status="ok", - detail="Verbindung bestaetigt.", + detail="Verbindung bestätigt.", license_update=update, ) diff --git a/apps/tesm-license/src/tesm_license/services/issuance.py b/apps/tesm-license/src/tesm_license/services/issuance.py index baf3b1e..955004f 100644 --- a/apps/tesm-license/src/tesm_license/services/issuance.py +++ b/apps/tesm-license/src/tesm_license/services/issuance.py @@ -48,7 +48,7 @@ def validate_email(value: str, *, required: bool) -> str: # sollen weiter bearbeitbar bleiben, statt ausgesperrt zu werden. return "" if not EMAIL_RE.match(text): - raise IssuanceError(f"{text!r} ist keine gueltige E-Mail-Adresse.") + raise IssuanceError(f"{text!r} ist keine gültige E-Mail-Adresse.") return text @@ -137,7 +137,7 @@ def delete_customer(conn: sqlite3.Connection, customer_id: int) -> None: ) if int(tickets or 0): raise IssuanceError( - f"An diesem Kunden haengen noch {tickets} Ticket(s). Bitte zuerst entfernen." + f"An diesem Kunden hängen noch {tickets} Ticket(s). Bitte zuerst entfernen." ) conn.execute("DELETE FROM license_customers WHERE id=?", (customer_id,)) @@ -224,7 +224,7 @@ def create_ticket( raise IssuanceError("Unbekannter Lizenztyp.") if role not in lic.LICENSE_ROLES or role == "master": # "master" entsteht ausschliesslich lokal per Bootstrap-Skript. - raise IssuanceError("Unzulaessige Rolle.") + raise IssuanceError("Unzulässige Rolle.") # Trial + unbefristet ist ein sinnloser Zustand: eine permanente Lizenz ohne # Module. Diese Regel wird an drei Stellen geprueft (hier, beim Bearbeiten @@ -290,7 +290,7 @@ def update_ticket( if license_type != str(ticket["type"]): changes.append(f"Typ: {ticket['type']} -> {license_type}") if new_modules != str(ticket["modules"]): - changes.append("Module geaendert") + changes.append("Module geändert") if int(valid_days) != int(ticket["valid_days"]): changes.append(f"Laufzeit: {ticket['valid_days']} -> {valid_days} Tage") if bool(lifetime) != bool(ticket["lifetime"]): @@ -428,7 +428,7 @@ def issue_from_ticket( role = str(ticket["role"]) if as_trial and role != "customer": raise IssuanceError( - "Eine Testlizenz gibt es nur fuer Kundeninstanzen, nicht fuer Lizenzserver." + "Eine Testlizenz gibt es nur für Kundeninstanzen, nicht für Lizenzserver." ) if as_trial: @@ -452,9 +452,9 @@ def issue_from_ticket( # hier, an der einzigen Stelle, durch die jede Ausstellung laeuft. if not str(master_endpoint or "").strip(): raise IssuanceError( - "Es ist kein oeffentlicher Endpunkt hinterlegt (Einstellungen -> Anbieter). " - "Ohne ihn koennte sich diese Lizenz nie online aktivieren -- der Endpunkt " - "wird in die Lizenz signiert und laesst sich spaeter nicht nachtragen." + "Es ist kein öffentlicher Endpunkt hinterlegt (Einstellungen -> Anbieter). " + "Ohne ihn könnte sich diese Lizenz nie online aktivieren – der Endpunkt " + "wird in die Lizenz signiert und lässt sich später nicht nachtragen." ) customer = get_customer(conn, int(ticket["customer_id"])) @@ -550,7 +550,7 @@ def delivery_bundle(conn: sqlite3.Connection, license_id: str, *, keystore: Any) bundle["client_key"] = keystore.decrypt(encrypted, aad=client_key_aad(license_id)) except Exception as exc: # noqa: BLE001 raise IssuanceError( - "Der Client-Schluessel dieser Lizenz konnte nicht entschluesselt werden. " + "Der Client-Schlüssel dieser Lizenz konnte nicht entschlüsselt werden. " "Bitte eine neue Lizenz ausstellen." ) from exc return bundle @@ -580,7 +580,7 @@ def delete_license(conn: sqlite3.Connection, license_id: str) -> None: raise IssuanceError("Diese Lizenz existiert nicht.") if str(row["status"]) not in {"revoked", "issued"}: raise IssuanceError( - "Eine aktive Lizenz kann nicht geloescht werden. Bitte zuerst widerrufen." + "Eine aktive Lizenz kann nicht gelöscht werden. Bitte zuerst widerrufen." ) conn.execute("DELETE FROM licenses WHERE license_id=?", (license_id,)) diff --git a/apps/tesm-license/src/tesm_license/services/legacy_import.py b/apps/tesm-license/src/tesm_license/services/legacy_import.py index 585d7e3..50d2a5f 100644 --- a/apps/tesm-license/src/tesm_license/services/legacy_import.py +++ b/apps/tesm-license/src/tesm_license/services/legacy_import.py @@ -61,7 +61,7 @@ class Report: def as_text(self) -> str: lines = [ - f"Signaturschluessel uebernommen: {'ja' if self.key_adopted else 'nein'}" + f"Signaturschlüssel übernommen: {'ja' if self.key_adopted else 'nein'}" + (f" ({self.key_id})" if self.key_id else ""), f"Kunden : {self.customers}", f"Tickets : {self.tickets}", @@ -69,7 +69,7 @@ class Report: ] if self.reissued: lines.append("") - lines.append("Neu signiert -- diese Kunden brauchen eine neue Lizenzdatei:") + lines.append("Neu signiert – diese Kunden brauchen eine neue Lizenzdatei:") lines.extend(f" * {entry}" for entry in self.reissued) if self.skipped: lines.append("") @@ -95,7 +95,7 @@ def open_legacy(root: Path) -> sqlite3.Connection: """Oeffnet die alte Datenbank **nur lesend**.""" path = Path(root) / "sqlite.db" if not path.is_file(): - raise LegacyImportError(f"Unter {path} liegt keine Datenbank der Vorgaengerfassung.") + raise LegacyImportError(f"Unter {path} liegt keine Datenbank der Vorgängerfassung.") try: conn = sqlite3.connect(f"file:{path}?mode=ro", uri=True) except sqlite3.Error as exc: @@ -107,7 +107,7 @@ def open_legacy(root: Path) -> sqlite3.Connection: if missing: conn.close() raise LegacyImportError( - f"Das sieht nicht nach einem Lizenzserver aus -- es fehlen: {', '.join(sorted(missing))}" + f"Das sieht nicht nach einem Lizenzserver aus – es fehlen: {', '.join(sorted(missing))}" ) return conn @@ -123,7 +123,7 @@ def _standard_b64(wert: str, feld: str) -> str: """ roh = wert.strip() if not roh: - raise LegacyImportError(f"Im alten Signaturschluessel fehlt {feld!r}.") + raise LegacyImportError(f"Im alten Signaturschlüssel fehlt {feld!r}.") # Fehlende Polsterung ergaenzen -- der Vorgaenger schrieb sie mit, andere # Werkzeuge lassen sie weg. gepolstert = roh + "=" * (-len(roh) % 4) @@ -138,29 +138,29 @@ def _standard_b64(wert: str, feld: str) -> str: continue if len(rohbytes) != 32: raise LegacyImportError( - f"{feld} im alten Signaturschluessel ist {len(rohbytes)} Byte lang, " + f"{feld} im alten Signaturschlüssel ist {len(rohbytes)} Byte lang, " "erwartet werden 32 (Ed25519)." ) return base64.b64encode(rohbytes).decode("ascii") - raise LegacyImportError(f"{feld} im alten Signaturschluessel ist kein Base64.") + raise LegacyImportError(f"{feld} im alten Signaturschlüssel ist kein Base64.") def read_legacy_key(root: Path) -> tuple[str, str]: """Liest den alten Signaturschluessel. Rueckgabe: ``(privat, oeffentlich)``.""" path = Path(root) / "master_signing_key.json" if not path.is_file(): - raise LegacyImportError(f"Der alte Signaturschluessel {path} fehlt.") + raise LegacyImportError(f"Der alte Signaturschlüssel {path} fehlt.") try: data = json.loads(path.read_text(encoding="utf-8")) except (OSError, ValueError) as exc: - raise LegacyImportError(f"Der alte Signaturschluessel ist unlesbar: {exc}") from exc + raise LegacyImportError(f"Der alte Signaturschlüssel ist unlesbar: {exc}") from exc # Die Vorgaengerfassung nannte die Felder "private"/"public" und kodierte # sie URL-sicher. private = str(data.get("private") or data.get("private_key") or "") public = str(data.get("public") or data.get("public_key") or "") if not private or not public: - raise LegacyImportError("Der alte Signaturschluessel ist unvollstaendig.") + raise LegacyImportError("Der alte Signaturschlüssel ist unvollständig.") private = _standard_b64(private, "private") public = _standard_b64(public, "public") # Alles, was aus der Protokollschicht kommt, wird hier zu einer Meldung im @@ -170,11 +170,11 @@ def read_legacy_key(root: Path) -> tuple[str, str]: try: passt = licsrv.public_from_private(private) == public except (ValueError, TypeError) as exc: - raise LegacyImportError(f"Der alte Signaturschluessel ist unbrauchbar: {exc}") from exc + raise LegacyImportError(f"Der alte Signaturschlüssel ist unbrauchbar: {exc}") from exc if not passt: raise LegacyImportError( - "Im alten Signaturschluessel passen privater und oeffentlicher Teil nicht " - "zueinander. Die Datei ist beschaedigt." + "Im alten Signaturschlüssel passen privater und öffentlicher Teil nicht " + "zueinander. Die Datei ist beschädigt." ) return private, public @@ -190,16 +190,16 @@ def adopt_key(private: str, public: str, target: Path) -> str: if Path(target).is_file(): raise LegacyImportError( - f"Unter {target} liegt bereits ein Signaturschluessel. Er wird nicht " - "ueberschrieben -- das wuerde jede damit ausgestellte Lizenz unbrauchbar " - "machen. Fuer eine Uebernahme bitte eine frische Installation verwenden." + f"Unter {target} liegt bereits ein Signaturschlüssel. Er wird nicht " + "überschrieben – das würde jede damit ausgestellte Lizenz unbrauchbar " + "machen. Für eine Übernahme bitte eine frische Installation verwenden." ) payload = { "private_key": private, "public_key": public, "key_id": lic.key_id(public), "created_at": _now(), - "origin": "Uebernommen aus der Vorgaengerfassung.", + "origin": "Übernommen aus der Vorgängerfassung.", } write_private_file(Path(target), json.dumps(payload, indent=2, sort_keys=True) + "\n") return lic.key_id(public) @@ -309,7 +309,7 @@ def import_all( ) if existing is not None: customer_map[int(row["id"])] = int(existing["id"]) - report.warnings.append(f"Kunde {name!r} war schon vorhanden -- nicht doppelt angelegt.") + report.warnings.append(f"Kunde {name!r} war schon vorhanden – nicht doppelt angelegt.") continue new_id = issuance.create_customer( conn, @@ -331,7 +331,7 @@ def import_all( for row in legacy.execute("SELECT * FROM license_tickets ORDER BY created_at"): customer_id = customer_map.get(int(row["customer_id"] or 0)) if customer_id is None: - report.skipped.append(f"Ticket {str(row['id'])[:12]} -- Kunde nicht uebernommen") + report.skipped.append(f"Ticket {str(row['id'])[:12]} -- Kunde nicht übernommen") continue if Database.one(conn, "SELECT id FROM license_tickets WHERE id=?", (row["id"],)): continue @@ -348,7 +348,7 @@ def import_all( int(row["lifetime"] or 0), str(row["status"] or "open"), str(row["role"] or "customer"), - "Aus der Vorgaengerfassung uebernommen.", + "Aus der Vorgängerfassung übernommen.", str(row["created_by"] or actor), str(row["created_at"] or _now()), _now(), @@ -361,7 +361,7 @@ def import_all( license_id = str(row["license_id"] or "") customer_id = customer_map.get(int(row["customer_id"] or 0)) if not license_id or customer_id is None: - report.skipped.append(f"Lizenz {license_id[:12] or '?'} -- Kunde nicht uebernommen") + report.skipped.append(f"Lizenz {license_id[:12] or '?'} -- Kunde nicht übernommen") continue if Database.one(conn, "SELECT id FROM licenses WHERE license_id=?", (license_id,)): continue @@ -371,7 +371,7 @@ def import_all( modules = _modules(row["modules"]) if not reissue: - report.skipped.append(f"{customer['name']} / {license_id[:12]} -- nur gezaehlt") + report.skipped.append(f"{customer['name']} / {license_id[:12]} -- nur gezählt") report.licenses += 1 continue @@ -383,7 +383,7 @@ def import_all( if not lifetime and valid_days <= 0: report.skipped.append( f"{customer['name']} / {license_id[:12]} -- abgelaufen am " - f"{str(row['expires_at'])[:10]}, nicht uebernommen" + f"{str(row['expires_at'])[:10]}, nicht übernommen" ) continue @@ -444,7 +444,7 @@ def import_all( if report.reissued: report.warnings.append( "Die neu signierten Lizenzen sind noch nicht aktiviert: die Bindung an eine " - "Maschine gilt nur fuer die alte Datei. Nach dem Einspielen aktiviert der " + "Maschine gilt nur für die alte Datei. Nach dem Einspielen aktiviert der " "Kunde erneut." ) return report diff --git a/apps/tesm-license/src/tesm_license/services/mail.py b/apps/tesm-license/src/tesm_license/services/mail.py index 4b4f65b..7233815 100644 --- a/apps/tesm-license/src/tesm_license/services/mail.py +++ b/apps/tesm-license/src/tesm_license/services/mail.py @@ -92,7 +92,7 @@ def _secret(conn: sqlite3.Connection) -> str: return core().keystore.decrypt(str(value), aad=AAD_GRAPH_SECRET) except Exception as exc: # noqa: BLE001 raise MailError( - "Das Client-Secret konnte nicht entschluesselt werden -- bitte neu eintragen." + "Das Client-Secret konnte nicht entschlüsselt werden – bitte neu eintragen." ) from exc @@ -162,7 +162,7 @@ def fetch_token(conn: sqlite3.Connection) -> str: def test_connection(conn: sqlite3.Connection) -> dict[str, Any]: """Holt nur ein Token -- verschickt bewusst keine echte Nachricht.""" if not is_enabled(conn): - return {"ok": False, "message": "Der E-Mail-Versand ist nicht vollstaendig konfiguriert."} + return {"ok": False, "message": "Der E-Mail-Versand ist nicht vollständig konfiguriert."} try: fetch_token(conn) except MailError as exc: diff --git a/apps/tesm-license/src/tesm_license/services/master_key.py b/apps/tesm-license/src/tesm_license/services/master_key.py index 8ff9402..c3cc7f0 100644 --- a/apps/tesm-license/src/tesm_license/services/master_key.py +++ b/apps/tesm-license/src/tesm_license/services/master_key.py @@ -48,22 +48,22 @@ class MasterKey: def load(path: Path) -> MasterKey: if not path.is_file(): raise MasterKeyError( - f"Der Signaturschluessel {path} fehlt. Ohne ihn kann keine Lizenz ausgestellt " + f"Der Signaturschlüssel {path} fehlt. Ohne ihn kann keine Lizenz ausgestellt " "werden. Erzeugen Sie ihn mit 'tesm-license-admin init-key'." ) try: data = json.loads(path.read_text(encoding="utf-8")) except (OSError, ValueError) as exc: - raise MasterKeyError(f"Der Signaturschluessel ist unlesbar: {exc}") from exc + raise MasterKeyError(f"Der Signaturschlüssel ist unlesbar: {exc}") from exc private = str(data.get("private_key") or "") public = str(data.get("public_key") or "") if not private or not public: - raise MasterKeyError("Der Signaturschluessel ist unvollstaendig.") + raise MasterKeyError("Der Signaturschlüssel ist unvollständig.") if licsrv.public_from_private(private) != public: raise MasterKeyError( - "Der oeffentliche Teil des Signaturschluessels passt nicht zum privaten. " - "Die Datei ist beschaedigt oder wurde manipuliert." + "Der öffentliche Teil des Signaturschlüssels passt nicht zum privaten. " + "Die Datei ist beschädigt oder wurde manipuliert." ) return MasterKey( private_key=private, public_key=public, created_at=str(data.get("created_at") or "") @@ -87,7 +87,7 @@ def ensure(path: Path) -> MasterKey: } write_private_file(path, json.dumps(payload, indent=2, sort_keys=True) + "\n") logger.warning( - "Neuer Signaturschluessel erzeugt (%s). Diese Datei ist unwiederbringlich -- " + "Neuer Signaturschlüssel erzeugt (%s). Diese Datei ist unwiederbringlich – " "bitte sofort gesichert ablegen: %s", lic.key_id(public), path, @@ -98,7 +98,7 @@ def ensure(path: Path) -> MasterKey: def health(path: Path) -> dict[str, object]: """Zustand fuer die Diagnoseseite.""" if not path.is_file(): - return {"present": False, "problems": ["Signaturschluessel fehlt"]} + return {"present": False, "problems": ["Signaturschlüssel fehlt"]} problems = check_private_file(path) try: key = load(path) diff --git a/apps/tesm-license/src/tesm_license/services/transfer.py b/apps/tesm-license/src/tesm_license/services/transfer.py index 1ffd0ea..e28c9b3 100644 --- a/apps/tesm-license/src/tesm_license/services/transfer.py +++ b/apps/tesm-license/src/tesm_license/services/transfer.py @@ -41,11 +41,11 @@ APP_TAG = "tesm-license" CATEGORIES: tuple[Category, ...] = ( Category("customers", "Kunden", "Stammdaten der Kundeninstanzen"), - Category("tickets", "Tickets", "Anspruechte inklusive Self-Service-Schluessel", sensitive=True), + Category("tickets", "Tickets", "Ansprüche inklusive Self-Service-Schlüssel", sensitive=True), Category( "licenses", "Lizenzen", - "Ausgestellte Lizenzen mit signiertem Dokument und verschluesseltem Client-Schluessel", + "Ausgestellte Lizenzen mit signiertem Dokument und verschlüsseltem Client-Schlüssel", sensitive=True, ), Category("users", "Benutzer", "Lokale Konten mit Passwort-Hash", admin_only=True), diff --git a/apps/tesm-license/src/tesm_license/settings_spec.py b/apps/tesm-license/src/tesm_license/settings_spec.py index f442e02..f326e99 100644 --- a/apps/tesm-license/src/tesm_license/settings_spec.py +++ b/apps/tesm-license/src/tesm_license/settings_spec.py @@ -25,8 +25,8 @@ VENDOR_SETTINGS: tuple[SettingSpec, ...] = ( "master_endpoint", "", "str", - "Oeffentlicher Endpunkt dieses Lizenzservers", - hint="Basis-URL, unter der Kundeninstanzen diesen Server erreichen -- z. B. " + "Öffentlicher Endpunkt dieses Lizenzservers", + hint="Basis-URL, unter der Kundeninstanzen diesen Server erreichen – z. B. " "https://lizenz.firma.de. Wird in jede Lizenz eingebettet und fuer alle " "kundengerichteten Links verwendet.", section="vendor", @@ -38,8 +38,8 @@ PORTAL_SETTINGS: tuple[SettingSpec, ...] = ( "portal_link_minutes", 30, "int", - "Gueltigkeit des Zugangslinks (Minuten)", - hint="Der Link ist zusaetzlich nur einmal verwendbar.", + "Gültigkeit des Zugangslinks (Minuten)", + hint="Der Link ist zusätzlich nur einmal verwendbar.", section="portal", minimum=5, maximum=1440, @@ -48,9 +48,9 @@ PORTAL_SETTINGS: tuple[SettingSpec, ...] = ( "portal_session_minutes", 60, "int", - "Gueltigkeit der Portalsitzung (Minuten)", - hint="Zweites, unabhaengiges Zeitfenster: eines fuer das Anklicken des Links, " - "eines fuer die daraus entstehende Sitzung.", + "Gültigkeit der Portalsitzung (Minuten)", + hint="Zweites, unabhängiges Zeitfenster: eines für das Anklicken des Links, " + "eines für die daraus entstehende Sitzung.", section="portal", minimum=5, maximum=1440, @@ -64,7 +64,7 @@ PORTAL_SETTINGS: tuple[SettingSpec, ...] = ( "bool", "Self-Service-Link je Ticket aktiv", hint="Erlaubt dem Kunden, seine Lizenz selbst herunterzuladen, zu widerrufen und " - "neu auszustellen -- ohne Anmeldung, allein ueber den unratbaren Ticket-Link.", + "neu auszustellen – ohne Anmeldung, allein über den unratbaren Ticket-Link.", section="portal", ), ) @@ -83,8 +83,8 @@ DEFAULT_TICKET_SETTINGS: tuple[SettingSpec, ...] = ( "allow_customer_revoke", True, "bool", - "Kunden duerfen ihre Lizenz selbst widerrufen", - hint="Notwendig fuer einen Systemwechsel ohne Support-Anruf.", + "Kunden dürfen ihre Lizenz selbst widerrufen", + hint="Notwendig für einen Systemwechsel ohne Support-Anruf.", section="licensing", ), ) diff --git a/apps/tesm-license/src/tesm_license/templates/license/license_detail.html b/apps/tesm-license/src/tesm_license/templates/license/license_detail.html index 92e2cd3..8e47fb1 100644 --- a/apps/tesm-license/src/tesm_license/templates/license/license_detail.html +++ b/apps/tesm-license/src/tesm_license/templates/license/license_detail.html @@ -170,7 +170,7 @@
{{ icon('file-text', size=17) }} Signiertes Dokument
-
Genau so, wie es signiert wurde -- jede Änderung bricht die Signatur
+
Genau so, wie es signiert wurde – jede Änderung bricht die Signatur
{{ icon('chevron-down', size=16) }}
@@ -214,7 +214,7 @@

- Das Ticket bleibt offen -- eine neue Lizenz kann sofort ausgestellt werden. Genau so + Das Ticket bleibt offen – eine neue Lizenz kann sofort ausgestellt werden. Genau so läuft ein Systemwechsel.

{% endif %} diff --git a/apps/tesm-license/src/tesm_license/templates/license/portal_request.html b/apps/tesm-license/src/tesm_license/templates/license/portal_request.html index 6515f60..d8927ba 100644 --- a/apps/tesm-license/src/tesm_license/templates/license/portal_request.html +++ b/apps/tesm-license/src/tesm_license/templates/license/portal_request.html @@ -22,7 +22,7 @@

- Aus Datenschutzgründen bekommen Sie immer dieselbe Rückmeldung -- unabhängig davon, ob die + Aus Datenschutzgründen bekommen Sie immer dieselbe Rückmeldung – unabhängig davon, ob die Adresse bei uns bekannt ist.

{% endblock %} diff --git a/apps/tesm-license/src/tesm_license/templates/license/selfservice_offline.html b/apps/tesm-license/src/tesm_license/templates/license/selfservice_offline.html index 1f85c1c..76c88a3 100644 --- a/apps/tesm-license/src/tesm_license/templates/license/selfservice_offline.html +++ b/apps/tesm-license/src/tesm_license/templates/license/selfservice_offline.html @@ -15,7 +15,7 @@ {% if response_code %}
- Antwortcode -- in TESM eintragen + Antwortcode – in TESM eintragen
{{ response_code }}

- Tragen Sie ihn in TESM unter Verwaltung -- Lizenz bei + Tragen Sie ihn in TESM unter Verwaltung → Lizenz bei „Antwortcode des Lizenzservers“ ein. {% if summary.hostname %} Die Anfrage kam von {{ summary.hostname }}. diff --git a/apps/tesm-license/src/tesm_license/templates/license/settings_vendor.html b/apps/tesm-license/src/tesm_license/templates/license/settings_vendor.html index 9a9deb2..1087fe1 100644 --- a/apps/tesm-license/src/tesm_license/templates/license/settings_vendor.html +++ b/apps/tesm-license/src/tesm_license/templates/license/settings_vendor.html @@ -45,7 +45,7 @@ welche Basis-URL in kundengerichteten Links steht.', 'info') }} - Ein zu grosses Logo wird abgelehnt, die übrigen Angaben werden trotzdem gespeichert. + Ein zu großes Logo wird abgelehnt, die übrigen Angaben werden trotzdem gespeichert.

@@ -75,7 +75,7 @@ welche Basis-URL in kundengerichteten Links steht.', 'info') }}

Die Angaben werden zum Zeitpunkt der Ausstellung in die Lizenz kopiert und signiert. Eine - Änderung hier wirkt auf neue Lizenzen -- bestehende behalten ihren Stand, bis eine neue + Änderung hier wirkt auf neue Lizenzen – bestehende behalten ihren Stand, bis eine neue Revision signiert wird.

diff --git a/apps/tesm-license/src/tesm_license/templates/license/ticket_detail.html b/apps/tesm-license/src/tesm_license/templates/license/ticket_detail.html index 04cfec8..1125cc1 100644 --- a/apps/tesm-license/src/tesm_license/templates/license/ticket_detail.html +++ b/apps/tesm-license/src/tesm_license/templates/license/ticket_detail.html @@ -33,7 +33,7 @@
{{ icon('shield', size=17) }} Lizenz ausstellen
{% if has_open_license %} - Es existiert bereits eine offene Lizenz -- widerrufen Sie sie zuerst. + Es existiert bereits eine offene Lizenz – widerrufen Sie sie zuerst. {% else %} Erzeugt eine signierte Lizenzdatei mit den Werten dieses Tickets. {% endif %} @@ -128,7 +128,7 @@

Damit kann der Kunde seine Lizenz herunterladen, bei einem Systemwechsel freigeben und - neu ausstellen. Der Link enthält 192 Bit Zufall und ist nicht ratbar -- behandeln Sie + neu ausstellen. Der Link enthält 192 Bit Zufall und ist nicht ratbar – behandeln Sie ihn wie ein Passwort.

diff --git a/apps/tesm-license/src/tesm_license/templates/license/ticket_new.html b/apps/tesm-license/src/tesm_license/templates/license/ticket_new.html index 6f74d1a..8e7914d 100644 --- a/apps/tesm-license/src/tesm_license/templates/license/ticket_new.html +++ b/apps/tesm-license/src/tesm_license/templates/license/ticket_new.html @@ -32,7 +32,7 @@ {% endfor %} - Der Kunde ist nach dem Anlegen unveränderlich -- der Self-Service-Link ist an ihn + Der Kunde ist nach dem Anlegen unveränderlich – der Self-Service-Link ist an ihn gebunden. @@ -77,7 +77,7 @@
{{ icon('share', size=17) }} Lizenzserver-Lizenz
- Erzeugt eine neue Vertrauensgrenze -- der Empfänger verwaltet danach eigene Kunden + Erzeugt eine neue Vertrauensgrenze – der Empfänger verwaltet danach eigene Kunden
@@ -103,7 +103,7 @@
  1. Ticket anlegen (dieser Schritt).
  2. -
  3. Auf der Ticketseite eine Lizenz ausstellen -- optional als 30-Tage-Testlizenz mit +
  4. Auf der Ticketseite eine Lizenz ausstellen – optional als 30-Tage-Testlizenz mit allen Modulen.
  5. Lizenzdatei herunterladen oder per E-Mail versenden.
  6. Der Kunde spielt sie ein und aktiviert sie; die Aktivierung bindet sie an seine @@ -112,7 +112,7 @@

    Der Self-Service-Link des Tickets erlaubt dem Kunden, seine Lizenz selbst - herunterzuladen und bei einem Systemwechsel freizugeben -- ohne Support-Anruf. + herunterzuladen und bei einem Systemwechsel freizugeben – ohne Support-Anruf.