Version 2.7.2: Umlaute in der Oberflaeche, Einbindungen an der Sitzung
This commit is contained in:
1 parent
e5ae10814f
commit
35247c895b
72 files changed
+825
-470
No files matched your search
@@ -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 = [
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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"))
|
||||
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
@@ -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"))
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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", "*"),
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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,))
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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",
|
||||
),
|
||||
)
|
||||
|
||||
@@ -170,7 +170,7 @@
|
||||
<summary class="card__header" style="cursor:pointer">
|
||||
<div class="flex-1">
|
||||
<div class="card__title">{{ icon('file-text', size=17) }} Signiertes Dokument</div>
|
||||
<div class="card__sub">Genau so, wie es signiert wurde -- jede Änderung bricht die Signatur</div>
|
||||
<div class="card__sub">Genau so, wie es signiert wurde – jede Änderung bricht die Signatur</div>
|
||||
</div>
|
||||
{{ icon('chevron-down', size=16) }}
|
||||
</summary>
|
||||
@@ -214,7 +214,7 @@
|
||||
<button class="btn btn--sm btn--danger btn--block" type="button"
|
||||
data-dialog-open="revoke-dialog">Widerrufen</button>
|
||||
<p class="fs-xs faint">
|
||||
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.
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
</form>
|
||||
|
||||
<p class="auth-footnote">
|
||||
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.
|
||||
</p>
|
||||
{% endblock %}
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
{% if response_code %}
|
||||
<div class="field">
|
||||
<span class="field__label">Antwortcode -- in TESM eintragen</span>
|
||||
<span class="field__label">Antwortcode – in TESM eintragen</span>
|
||||
<pre class="log-view" id="offline-answer" style="max-height:16dvh">{{ response_code }}</pre>
|
||||
<div class="row row--end mt-2">
|
||||
<button class="btn btn--sm" type="button" data-copy="#offline-answer">
|
||||
@@ -23,7 +23,7 @@
|
||||
</button>
|
||||
</div>
|
||||
<p class="fs-sm muted mt-2">
|
||||
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 <span class="mono">{{ summary.hostname }}</span>.
|
||||
|
||||
@@ -45,7 +45,7 @@ welche Basis-URL in kundengerichteten Links steht.', 'info') }}
|
||||
<input class="input" type="file" name="vendor_logo" accept="image/png,image/jpeg,image/svg+xml"
|
||||
{% if not current_user.has('settings_vendor.edit') %}disabled{% endif %}>
|
||||
<span class="field__hint">
|
||||
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.
|
||||
</span>
|
||||
</div>
|
||||
</fieldset>
|
||||
@@ -75,7 +75,7 @@ welche Basis-URL in kundengerichteten Links steht.', 'info') }}
|
||||
<div class="divider"></div>
|
||||
<p class="fs-sm muted">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
<div class="card__title">{{ icon('shield', size=17) }} Lizenz ausstellen</div>
|
||||
<div class="card__sub">
|
||||
{% 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 @@
|
||||
</div>
|
||||
<p class="fs-xs faint">
|
||||
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.
|
||||
</p>
|
||||
<div class="divider"></div>
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
{% endfor %}
|
||||
</select>
|
||||
<span class="field__hint">
|
||||
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.
|
||||
</span>
|
||||
</label>
|
||||
@@ -77,7 +77,7 @@
|
||||
<div class="flex-1">
|
||||
<div class="card__title">{{ icon('share', size=17) }} Lizenzserver-Lizenz</div>
|
||||
<div class="card__sub">
|
||||
Erzeugt eine neue Vertrauensgrenze -- der Empfänger verwaltet danach eigene Kunden
|
||||
Erzeugt eine neue Vertrauensgrenze – der Empfänger verwaltet danach eigene Kunden
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -103,7 +103,7 @@
|
||||
<div class="card__body">
|
||||
<ol class="stack stack--tight fs-sm muted" style="list-style:decimal;padding-left:1.2em">
|
||||
<li>Ticket anlegen (dieser Schritt).</li>
|
||||
<li>Auf der Ticketseite eine Lizenz ausstellen -- optional als 30-Tage-Testlizenz mit
|
||||
<li>Auf der Ticketseite eine Lizenz ausstellen – optional als 30-Tage-Testlizenz mit
|
||||
allen Modulen.</li>
|
||||
<li>Lizenzdatei herunterladen oder per E-Mail versenden.</li>
|
||||
<li>Der Kunde spielt sie ein und aktiviert sie; die Aktivierung bindet sie an seine
|
||||
@@ -112,7 +112,7 @@
|
||||
<div class="divider"></div>
|
||||
<p class="fs-sm muted">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
<div class="card__footer">
|
||||
|
||||
Reference in new issue
Block a user