Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11a55b20f8 | ||
|
|
ed17bce40a | ||
|
|
5282cd8f2d |
No files matched your search
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "tesm"
|
||||
version = "2.6.0"
|
||||
version = "2.7.1"
|
||||
description = "TESM -- Verwaltung und Ueberwachung von PoE-Endgeraeten und Aruba-Switchen"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
|
||||
@@ -215,6 +215,22 @@ def upload(token: str):
|
||||
return redirect(url_for("fileshare.browse", share=token, path=relative))
|
||||
|
||||
|
||||
@bp.get("/<token>/signature")
|
||||
@permission_required("fileshare.view")
|
||||
def signature_state(token: str):
|
||||
"""Passt die Datei noch zu ihrem Eintrag im Aenderungsprotokoll?
|
||||
|
||||
Die Vorschau fragt das beim Oeffnen. Eine unterschriebene Fassung, die
|
||||
seither veraendert wurde, sieht man einer PDF sonst nicht an -- und genau
|
||||
das ist die Frage, die man sich vor einer Unterschrift stellt.
|
||||
"""
|
||||
mount = _mount_or_404(token)
|
||||
try:
|
||||
return service.signaturstand(db(), mount, request.args.get("path") or "")
|
||||
except service.FileshareError as exc:
|
||||
return {"error": str(exc)}, 400
|
||||
|
||||
|
||||
@bp.get("/<token>/folders")
|
||||
@permission_required("fileshare.view")
|
||||
def folders(token: str):
|
||||
@@ -304,8 +320,8 @@ def save_form(token: str):
|
||||
ziel = zielordner / name
|
||||
if ziel.exists() and not ersetzen:
|
||||
return {
|
||||
"error": f"'{name}' gibt es dort schon. Anderen Namen waehlen oder "
|
||||
"'Vorhandene ersetzen' ankreuzen."
|
||||
"error": f"In '{ziel_mount.label}' gibt es '{name}' schon. Anderen "
|
||||
"Namen waehlen oder 'Vorhandene ersetzen' ankreuzen."
|
||||
}, 409
|
||||
else:
|
||||
# Ohne Angabe wie bisher: neben das Original, ohne etwas zu ueberschreiben.
|
||||
@@ -332,6 +348,19 @@ def save_form(token: str):
|
||||
beschreibung,
|
||||
severity="notice",
|
||||
)
|
||||
if unterschriften:
|
||||
# Zusaetzlich ins Signaturverzeichnis: das Protokoll wird irgendwann
|
||||
# archiviert, die Frage "ist diese Datei noch die unterschriebene?"
|
||||
# stellt sich aber noch Jahre spaeter.
|
||||
with transaction() as conn:
|
||||
service.signatur_merken(
|
||||
conn,
|
||||
sha256=pruefsumme,
|
||||
label=ziel_mount.label,
|
||||
pfad=wohin,
|
||||
actor=current_user.username,
|
||||
namen=unterschriften,
|
||||
)
|
||||
return {"message": f"Gespeichert als {wohin}.", "path": wohin, "sha256": pruefsumme}
|
||||
|
||||
|
||||
|
||||
@@ -308,6 +308,69 @@ INSERT OR IGNORE INTO group_permissions (group_id, permission)
|
||||
"""
|
||||
|
||||
|
||||
# Die Pruefung "unveraendert seit der Unterschrift" hing am Aenderungsprotokoll
|
||||
# -- und das wird ab 20.000 Zeilen tageweise in Dateien ausgelagert. Mit dem
|
||||
# ausgelagerten Tag waere die Pruefsumme aus der Datenbank verschwunden und
|
||||
# jede aeltere unterschriebene Fassung waere still zu "unbekannt" geworden.
|
||||
# Schlimmer noch: auch eine **veraenderte** Fassung, denn ohne Vergleichswert
|
||||
# gibt es keinen Widerspruch.
|
||||
#
|
||||
# Deshalb eine eigene, winzige Tabelle: eine Zeile je Unterschrift, nie
|
||||
# archiviert. Sie ist ein Verzeichnis, keine Wahrheit -- die Wahrheit bleibt
|
||||
# das Aenderungsprotokoll mit seiner Hash-Kette, auch im Archiv.
|
||||
V7 = """
|
||||
CREATE TABLE IF NOT EXISTS document_signatures (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sha256 TEXT NOT NULL,
|
||||
share_label TEXT NOT NULL DEFAULT '',
|
||||
path TEXT NOT NULL DEFAULT '',
|
||||
signed_at TEXT NOT NULL,
|
||||
actor TEXT NOT NULL DEFAULT '',
|
||||
names TEXT NOT NULL DEFAULT '',
|
||||
audit_id INTEGER
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_document_signatures_sha
|
||||
ON document_signatures(sha256);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_document_signatures_ort
|
||||
ON document_signatures(share_label, path);
|
||||
"""
|
||||
|
||||
|
||||
def uebernehme_signaturen(conn) -> None: # type: ignore[no-untyped-def]
|
||||
"""Traegt die bereits protokollierten Unterschriften ins Verzeichnis ein.
|
||||
|
||||
Ohne diesen Schritt waeren alle vor dem Update unterschriebenen Fassungen
|
||||
ab sofort "unbekannt" -- richtig waere das nicht, die Eintraege stehen ja
|
||||
noch da.
|
||||
"""
|
||||
from .services import fileshare as service
|
||||
|
||||
zeilen = conn.execute(
|
||||
"SELECT id, ts, actor, detail FROM audit_log WHERE action='fileshare.form_signed' "
|
||||
"ORDER BY id"
|
||||
).fetchall()
|
||||
for zeile in zeilen:
|
||||
detail = str(zeile["detail"] or "")
|
||||
gelesen = service.signaturdetail_lesen(detail)
|
||||
if not gelesen["sha256"]:
|
||||
continue
|
||||
conn.execute(
|
||||
"INSERT INTO document_signatures (sha256, share_label, path, signed_at, actor, "
|
||||
"names, audit_id) VALUES (?,?,?,?,?,?,?)",
|
||||
(
|
||||
gelesen["sha256"],
|
||||
gelesen["label"],
|
||||
gelesen["pfad"],
|
||||
str(zeile["ts"]),
|
||||
str(zeile["actor"] or ""),
|
||||
", ".join(gelesen["namen"]),
|
||||
int(zeile["id"]),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
TESM_MIGRATIONS: tuple[Migration, ...] = (
|
||||
Migration(version=1, name="baseline", sql=V1),
|
||||
Migration(version=2, name="seed-dhcp-options", fn=seed_dhcp_options),
|
||||
@@ -315,4 +378,5 @@ TESM_MIGRATIONS: tuple[Migration, ...] = (
|
||||
Migration(version=4, name="fileshare-pdf-rechte", sql=V4),
|
||||
Migration(version=5, name="terminal-getrennt", sql=V5),
|
||||
Migration(version=6, name="wartung-eigener-bereich", sql=V6),
|
||||
Migration(version=7, name="signaturverzeichnis", sql=V7, fn=uebernehme_signaturen),
|
||||
)
|
||||
@@ -21,6 +21,7 @@ Sicherheitsrelevante Aenderungen gegenueber dem Vorgaenger:
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import zlib
|
||||
import logging
|
||||
import mimetypes
|
||||
@@ -604,6 +605,135 @@ def resolve_path(mount: Mount, relative: str) -> Path:
|
||||
return target
|
||||
|
||||
|
||||
#: Wie eine unterschriebene Fassung im Aenderungsprotokoll steht. Die
|
||||
#: Pruefsumme ist der Anker -- sie steht im selben Eintrag wie der Name, den
|
||||
#: der Unterschreibende angegeben hat.
|
||||
SIGNATUR_AKTION = "fileshare.form_signed"
|
||||
|
||||
|
||||
def pruefsumme(pfad: Path) -> str:
|
||||
"""SHA-256 der Datei, blockweise gelesen."""
|
||||
kessel = hashlib.sha256()
|
||||
with pfad.open("rb") as strom:
|
||||
for block in iter(lambda: strom.read(1024 * 1024), b""):
|
||||
kessel.update(block)
|
||||
return kessel.hexdigest()
|
||||
|
||||
|
||||
def signaturdetail_lesen(detail: str) -> dict[str, Any]:
|
||||
"""Zerlegt die Protokollzeile einer Unterschrift.
|
||||
|
||||
Aufbau: ``<Freigabe>:<Pfad> sha256=<Summe> -- angegeben unterschrieben
|
||||
von: <Namen>``. Gelesen wird sie an zwei Stellen -- beim Uebernehmen
|
||||
alter Eintraege und wenn das Verzeichnis nichts weiss.
|
||||
"""
|
||||
ort, _, rest = detail.partition(" sha256=")
|
||||
label, _, pfad = ort.partition(":")
|
||||
summe = rest.split(" ", 1)[0].strip() if rest else ""
|
||||
return {
|
||||
"sha256": summe,
|
||||
"label": label.strip(),
|
||||
"pfad": pfad.strip(),
|
||||
"namen": _namen_aus_detail(detail),
|
||||
}
|
||||
|
||||
|
||||
def signatur_merken(
|
||||
conn: sqlite3.Connection,
|
||||
*,
|
||||
sha256: str,
|
||||
label: str,
|
||||
pfad: str,
|
||||
actor: str,
|
||||
namen: Iterable[str],
|
||||
audit_id: int | None = None,
|
||||
) -> None:
|
||||
"""Haelt eine Unterschrift im Verzeichnis fest.
|
||||
|
||||
Das Aenderungsprotokoll bleibt der Beleg -- mit Hash-Kette und, nach der
|
||||
Archivierung, in einer Datei. Dieses Verzeichnis ist nur der schnelle Weg
|
||||
dorthin und wird nie ausgelagert; eine Zeile misst ein paar hundert Byte.
|
||||
"""
|
||||
conn.execute(
|
||||
"INSERT INTO document_signatures (sha256, share_label, path, signed_at, actor, "
|
||||
"names, audit_id) VALUES (?,?,?,?,?,?,?)",
|
||||
(
|
||||
sha256,
|
||||
label,
|
||||
pfad,
|
||||
_iso(_now()),
|
||||
actor,
|
||||
", ".join(namen),
|
||||
audit_id,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def signaturstand(conn: sqlite3.Connection, mount: "Mount", relative: str) -> dict[str, Any]:
|
||||
"""Ist diese Datei die, die unterschrieben wurde?
|
||||
|
||||
Drei moegliche Antworten, und die dritte ist die wichtigste:
|
||||
|
||||
* ``unveraendert`` -- es gibt einen Protokolleintrag mit genau dieser
|
||||
Pruefsumme. Die Datei ist Byte fuer Byte die unterschriebene.
|
||||
* ``veraendert`` -- es gibt einen Eintrag zu genau diesem Ablageort, aber
|
||||
mit anderer Pruefsumme. Jemand hat nach der Unterschrift etwas an der
|
||||
Datei geaendert. Das ist der Fall, wegen dem es diese Pruefung gibt.
|
||||
* ``unbekannt`` -- kein Eintrag. Kein Verdacht: die meisten Dateien in
|
||||
einer Freigabe sind nie durch TESM gegangen.
|
||||
|
||||
Nachgeschlagen wird im Signaturverzeichnis (``document_signatures``) und
|
||||
nicht im Aenderungsprotokoll: das wird ab 20.000 Zeilen tageweise in
|
||||
Dateien ausgelagert, und mit dem ausgelagerten Tag waere die Pruefsumme aus
|
||||
der Datenbank verschwunden. Eine aeltere unterschriebene Fassung waere
|
||||
still zu "unbekannt" geworden -- und eine **veraenderte** ebenso, weil ohne
|
||||
Vergleichswert niemand widerspricht. Das Verzeichnis wird nie archiviert.
|
||||
"""
|
||||
ziel = resolve_path(mount, relative)
|
||||
if not ziel.is_file():
|
||||
return {"status": "unbekannt"}
|
||||
hash_ = pruefsumme(ziel)
|
||||
|
||||
zeile = Database.one(
|
||||
conn,
|
||||
"SELECT signed_at, actor, names FROM document_signatures WHERE sha256=? "
|
||||
"ORDER BY id DESC LIMIT 1",
|
||||
(hash_,),
|
||||
)
|
||||
if zeile is not None:
|
||||
return _stand("unveraendert", hash_, zeile)
|
||||
|
||||
# Nichts mit dieser Pruefsumme -- aber vielleicht etwas zu diesem Ort.
|
||||
zeile = Database.one(
|
||||
conn,
|
||||
"SELECT signed_at, actor, names FROM document_signatures WHERE share_label=? "
|
||||
"AND path=? ORDER BY id DESC LIMIT 1",
|
||||
(mount.label, relative),
|
||||
)
|
||||
if zeile is not None:
|
||||
return _stand("veraendert", hash_, zeile)
|
||||
return {"status": "unbekannt", "sha256": hash_}
|
||||
|
||||
|
||||
def _stand(status: str, hash_: str, zeile: Any) -> dict[str, Any]:
|
||||
namen = [teil.strip() for teil in str(zeile["names"] or "").split(",") if teil.strip()]
|
||||
return {
|
||||
"status": status,
|
||||
"sha256": hash_,
|
||||
"zeit": str(zeile["signed_at"]),
|
||||
"konto": str(zeile["actor"] or ""),
|
||||
"namen": namen,
|
||||
}
|
||||
|
||||
|
||||
def _namen_aus_detail(detail: str) -> list[str]:
|
||||
"""Die angegebenen Namen aus dem Protokolltext -- ohne sie zu deuten."""
|
||||
marke = "angegeben unterschrieben von: "
|
||||
if marke not in detail:
|
||||
return []
|
||||
return [teil.strip() for teil in detail.split(marke, 1)[1].split(",") if teil.strip()]
|
||||
|
||||
|
||||
def list_directory(mount: Mount, relative: str = "") -> dict[str, Any]:
|
||||
path = resolve_path(mount, relative)
|
||||
if not path.is_dir():
|
||||
|
||||
@@ -103,6 +103,7 @@
|
||||
<button class="link-icon file-name" type="button"
|
||||
data-dialog-open="file-preview"
|
||||
data-fill-Src="{{ url_for('fileshare.preview', token=token, path=entry.relative) }}"
|
||||
data-fill-Pruefung="{{ url_for('fileshare.signature_state', token=token, path=entry.relative) }}"
|
||||
data-fill-Kind="{{ entry.preview_kind }}"
|
||||
data-fill-Pfad="{{ entry.relative }}"
|
||||
data-fill-Name="{{ entry.name }}"
|
||||
@@ -131,6 +132,7 @@
|
||||
<button class="btn btn--sm btn--ghost" type="button"
|
||||
data-dialog-open="file-preview"
|
||||
data-fill-Src="{{ url_for('fileshare.preview', token=token, path=entry.relative) }}"
|
||||
data-fill-Pruefung="{{ url_for('fileshare.signature_state', token=token, path=entry.relative) }}"
|
||||
data-fill-Kind="{{ entry.preview_kind }}"
|
||||
data-fill-Pfad="{{ entry.relative }}"
|
||||
data-fill-Name="{{ entry.name }}"
|
||||
@@ -313,14 +315,28 @@
|
||||
<option value="fertig">Abgeschlossen – Felder gesperrt</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="check">
|
||||
<input type="checkbox" data-viewer-ersetzen>
|
||||
<span>Vorhandene ersetzen</span>
|
||||
</label>
|
||||
<button class="btn btn--sm btn--primary" type="button" data-viewer-jetzt>Speichern</button>
|
||||
<button class="btn btn--sm btn--ghost" type="button" data-viewer-abbrechen>Abbrechen</button>
|
||||
{# Haken über den Knöpfen und beides rechts: gelesen wird die Zeile von
|
||||
links nach rechts (wohin, wie es heißt, wie gespeichert), und die
|
||||
Entscheidung steht am Ende. #}
|
||||
<div class="viewer__saveaktionen">
|
||||
<label class="check" data-viewer-ersetzenfeld>
|
||||
<input type="checkbox" data-viewer-ersetzen>
|
||||
<span>Vorhandene ersetzen</span>
|
||||
</label>
|
||||
<div class="viewer__saveknoepfe">
|
||||
<button class="btn btn--sm btn--ghost" type="button" data-viewer-abbrechen>
|
||||
Abbrechen
|
||||
</button>
|
||||
<button class="btn btn--sm btn--primary" type="button" data-viewer-jetzt>
|
||||
Speichern
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{# Das Ergebnis der Pruefung steht über der Leiste – wer eine
|
||||
unterschriebene Fassung öffnet, soll es sehen, bevor er liest. #}
|
||||
<p class="viewer__pruefung" data-viewer-pruefung hidden></p>
|
||||
<div class="viewer__bar" data-viewer-leiste hidden>
|
||||
<button class="btn btn--sm btn--ghost" type="button" data-viewer-zoom="aus"
|
||||
aria-label="Verkleinern">{{ icon('minus', size=15) }}</button>
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "tesm-core"
|
||||
version = "2.6.0"
|
||||
version = "2.7.1"
|
||||
description = "Gemeinsames Anwendungsgeruest (Auth, RBAC, Navigation, Design-System, Migrationen) fuer TESM und TESM-Lizenzserver"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
|
||||
@@ -148,7 +148,7 @@ def create_base_app(
|
||||
#: Wortmarke, ohne Zeile darunter; die Marke selbst zeigen beide
|
||||
#: Anwendungen.
|
||||
auth_tagline: str = "",
|
||||
app_version: str = "2.6.0",
|
||||
app_version: str = "2.7.1",
|
||||
built_in_trusted_keys: tuple[str, ...] = (),
|
||||
enable_license: bool = True,
|
||||
template_folder: str | None = None,
|
||||
|
||||
@@ -847,6 +847,25 @@
|
||||
flex: 1 1 12rem;
|
||||
margin: 0;
|
||||
}
|
||||
/* Die Entscheidung steht rechts und der Haken darueber: er gehoert zum
|
||||
Speichern, nicht zu den Feldern davor. */
|
||||
.viewer__saveaktionen {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: var(--sp-2);
|
||||
}
|
||||
.viewer__saveknoepfe {
|
||||
display: flex;
|
||||
gap: var(--sp-2);
|
||||
}
|
||||
/* Nach "gibt es schon": der Haken ist die Antwort -- also faellt er auf. */
|
||||
.viewer__saveaktionen .check.is-gefragt {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 3px;
|
||||
border-radius: var(--r-xs);
|
||||
}
|
||||
/* Eigener Name: ".field--inline" gibt es schon und steht dort fuer
|
||||
"waagerecht" -- diese Regel haette sie app-weit umgestellt. */
|
||||
.viewer__feld {
|
||||
@@ -1439,3 +1458,50 @@
|
||||
pointer-events: none;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
/* -- Ergebnis der Signaturpruefung ---------------------------------------- */
|
||||
/* Der Schleier liegt ueber der Seite und nimmt keine Klicks an: ein
|
||||
unterschriebenes Dokument ist flach, aber bei einem Irrtum soll die
|
||||
Kennzeichnung nicht auch noch die Bedienung blockieren. */
|
||||
|
||||
.viewer__pdf--unveraendert {
|
||||
box-shadow: inset 0 0 0 3px var(--success);
|
||||
}
|
||||
|
||||
.viewer__pdf--unveraendert::after,
|
||||
.viewer__pdf--veraendert::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.viewer__pdf--unveraendert::after {
|
||||
background: rgba(47, 158, 68, 0.1);
|
||||
}
|
||||
|
||||
.viewer__pdf--veraendert {
|
||||
box-shadow: inset 0 0 0 3px var(--danger);
|
||||
}
|
||||
|
||||
.viewer__pdf--veraendert::after {
|
||||
background: rgba(201, 42, 42, 0.08);
|
||||
}
|
||||
|
||||
.viewer__pruefung {
|
||||
margin: 0;
|
||||
padding: 6px 12px;
|
||||
font-size: var(--fs-xs);
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.viewer__pruefung--gut {
|
||||
color: var(--success);
|
||||
background: rgba(47, 158, 68, 0.12);
|
||||
}
|
||||
|
||||
.viewer__pruefung--schlecht {
|
||||
color: var(--danger);
|
||||
background: rgba(201, 42, 42, 0.12);
|
||||
}
|
||||
@@ -601,9 +601,18 @@ async function pdfBetrachter(behaelter, seiten, url, basis, { formulare = true }
|
||||
const anteilY = (event.clientY - rechteck.top) / rechteck.height;
|
||||
const seite = await dokument.getPage(nummer);
|
||||
const blick = seite.getViewport({ scale: 1 });
|
||||
const breite = Math.min(170, blick.width * 0.35);
|
||||
const hoehe =
|
||||
(wartendeUnterschrift.hoehe / wartendeUnterschrift.breite) * breite;
|
||||
// Ein menschliches Mass: rund fuenf Zentimeter breit, hoechstens knapp
|
||||
// zwei hoch. Ohne die Hoehengrenze wird aus einem hohen Gekritzel ein
|
||||
// Stempel, der die halbe Seite einnimmt -- das Seitenverhaeltnis kommt ja
|
||||
// aus dem Zuschnitt, nicht aus der Vorlage.
|
||||
const MAX_BREITE = 150;
|
||||
const MAX_HOEHE = 50;
|
||||
let breite = Math.min(MAX_BREITE, blick.width * 0.3);
|
||||
let hoehe = (wartendeUnterschrift.hoehe / wartendeUnterschrift.breite) * breite;
|
||||
if (hoehe > MAX_HOEHE) {
|
||||
breite *= MAX_HOEHE / hoehe;
|
||||
hoehe = MAX_HOEHE;
|
||||
}
|
||||
const eintrag = {
|
||||
...wartendeUnterschrift,
|
||||
seite: nummer,
|
||||
@@ -699,6 +708,8 @@ export function initFilePreview(wurzel = document) {
|
||||
const ordnerFeld = stueck("ordner");
|
||||
const nameFeld = stueck("name");
|
||||
const ersetzenFeld = stueck("ersetzen");
|
||||
const ersetzenKasten = stueck("ersetzenfeld");
|
||||
const pruefzeile = stueck("pruefung");
|
||||
const jetzt = stueck("jetzt");
|
||||
const abbrechen = stueck("abbrechen");
|
||||
const modusFeld = stueck("modus");
|
||||
@@ -760,6 +771,7 @@ export function initFilePreview(wurzel = document) {
|
||||
};
|
||||
|
||||
const aufraeumen = () => {
|
||||
pruefungZuruecksetzen();
|
||||
bild.removeAttribute("src");
|
||||
bilder.zuruecksetzen();
|
||||
rahmen.src = "about:blank";
|
||||
@@ -777,6 +789,52 @@ export function initFilePreview(wurzel = document) {
|
||||
aktuell = null;
|
||||
};
|
||||
|
||||
/* Unterschrieben und unveraendert? Die Antwort kommt aus dem
|
||||
Aenderungsprotokoll: dort steht die Pruefsumme der abgelegten Fassung.
|
||||
Stimmt sie, bekommt die Seite einen gruenen Schleier -- man soll es sehen,
|
||||
bevor man liest, nicht danach. */
|
||||
|
||||
const pruefungZuruecksetzen = () => {
|
||||
pdfBehaelter.classList.remove("viewer__pdf--unveraendert", "viewer__pdf--veraendert");
|
||||
if (pruefzeile) {
|
||||
pruefzeile.hidden = true;
|
||||
pruefzeile.textContent = "";
|
||||
pruefzeile.className = "viewer__pruefung";
|
||||
}
|
||||
};
|
||||
|
||||
const pruefen = async (adresse) => {
|
||||
pruefungZuruecksetzen();
|
||||
if (!adresse) return;
|
||||
try {
|
||||
const stand = await window.tesm.api(adresse);
|
||||
if (stand.status === "unveraendert") {
|
||||
pdfBehaelter.classList.add("viewer__pdf--unveraendert");
|
||||
if (pruefzeile) {
|
||||
pruefzeile.hidden = false;
|
||||
pruefzeile.className = "viewer__pruefung viewer__pruefung--gut";
|
||||
const namen = (stand.namen || []).join(", ");
|
||||
pruefzeile.textContent =
|
||||
`Unveraendert seit der Unterschrift${namen ? ` von ${namen}` : ""}` +
|
||||
` -- Pruefsumme stimmt mit dem Aenderungsprotokoll ueberein.`;
|
||||
}
|
||||
} else if (stand.status === "veraendert") {
|
||||
pdfBehaelter.classList.add("viewer__pdf--veraendert");
|
||||
if (pruefzeile) {
|
||||
pruefzeile.hidden = false;
|
||||
pruefzeile.className = "viewer__pruefung viewer__pruefung--schlecht";
|
||||
pruefzeile.textContent =
|
||||
"Diese Datei wurde nach der Unterschrift veraendert -- die Pruefsumme " +
|
||||
"weicht vom Aenderungsprotokoll ab.";
|
||||
}
|
||||
}
|
||||
} catch (fehler) {
|
||||
// Keine Aussage ist besser als eine falsche: ohne Antwort bleibt die
|
||||
// Seite schlicht ungekennzeichnet.
|
||||
console.error("Signaturpruefung", fehler);
|
||||
}
|
||||
};
|
||||
|
||||
const oeffnen = async (ausloeser) => {
|
||||
aufraeumen();
|
||||
const art = ausloeser.dataset.fillKind || "text";
|
||||
@@ -792,6 +850,7 @@ export function initFilePreview(wurzel = document) {
|
||||
return;
|
||||
}
|
||||
if (hinweis) hinweis.textContent = "PDF wird geladen ...";
|
||||
pruefen(ausloeser.dataset.fillPruefung || "");
|
||||
try {
|
||||
pdf = await pdfBetrachter(pdfScroll, pdfSeiten, quelle, basis, {
|
||||
formulare: rumpf.dataset.viewerFormulare === "1",
|
||||
@@ -846,8 +905,18 @@ export function initFilePreview(wurzel = document) {
|
||||
// Ohne Endung: die haengt der Server an. Eine Datei, die auf ".pdf.txt"
|
||||
// oder auf gar nichts endet, oeffnet auf einer Freigabe nichts mehr.
|
||||
// Der Zusatz haelt die Vorlage heil, auch wenn jemand blind speichert.
|
||||
nameFeld.value = datei.replace(/\.pdf$/i, "") + "-ausgefuellt";
|
||||
// Der Zusatz sagt, was in der Datei steht: unterschrieben schlaegt
|
||||
// ausgefuellt. Und einmal reicht -- wer eine Fassung noch einmal oeffnet
|
||||
// und speichert, bekam sonst "...-ausgefuellt-ausgefuellt".
|
||||
const rumpfname = datei.replace(/\.pdf$/i, "");
|
||||
const zusatz = pdf?.unterschriften().length ? "-unterschrieben" : "-ausgefuellt";
|
||||
const bereits = new RegExp(`${zusatz}$`, "i").test(rumpfname);
|
||||
// Eine unterschriebene Fassung aus einer ausgefuellten: der alte Zusatz
|
||||
// haette sonst zweimal dran gestanden ("-ausgefuellt-unterschrieben").
|
||||
const blank = rumpfname.replace(/-(ausgefuellt|unterschrieben)$/i, "");
|
||||
nameFeld.value = bereits ? rumpfname : `${blank}${zusatz}`;
|
||||
ersetzenFeld.checked = false;
|
||||
ersetzenKasten?.classList.remove("is-gefragt");
|
||||
};
|
||||
|
||||
/* Unterschreiben: Name tippen, zeichnen, ins Dokument setzen. */
|
||||
@@ -973,6 +1042,12 @@ export function initFilePreview(wurzel = document) {
|
||||
// Zeile bleibt offen: meist ist es ein Name, der schon vergeben ist.
|
||||
console.error("Formular speichern", fehler);
|
||||
window.tesm.showToast(fehler.message || "Speichern fehlgeschlagen.", "danger");
|
||||
// Genau dieser Fall hat eine Antwort auf der Zeile -- dann zeigen wir
|
||||
// auch darauf, statt es bei einer Meldung zu belassen.
|
||||
if (fehler.status === 409) {
|
||||
ersetzenKasten?.classList.add("is-gefragt");
|
||||
ersetzenFeld?.focus();
|
||||
}
|
||||
} finally {
|
||||
jetzt.disabled = false;
|
||||
jetzt.textContent = vorher;
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "tesm-licensing"
|
||||
version = "2.6.0"
|
||||
version = "2.7.1"
|
||||
description = "Ed25519-basiertes Lizenz- und Aktivierungsprotokoll fuer TESM und TESM-Lizenzserver"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = ["cryptography>=42"]
|
||||
|
||||
Reference in new issue
Block a user