diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bdd382..a29b340 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,18 @@ und dieses Projekt folgt [Semantic Versioning](https://semver.org/lang/de/). ### Sicherheit +## [0.16.0] – 2026-09-18 + +### Hinzugefügt +- *Einstellungen → System*: Dashboard-Adresse, Melderhythmus und Selbstaktualisierung der Agents, kaufmännische Vorgaben, Sicherheitsgrenzen, Firmenangaben und Zeitzone (#025) +- Die Dashboard-Adresse und der Melderhythmus landen im ausgelieferten Installations-Einzeiler und im systemd-Timer (#025) +- OIDC lässt sich auf bestimmte E-Mail-Domänen begrenzen; das automatische Anlegen unbekannter Konten ist abschaltbar (#025) +- Nächtliches Aufräumen nach einstellbaren Aufbewahrungsfristen für Reports und Protokoll – der jüngste Report jedes Hosts bleibt erhalten (#025) +- Firmenangaben erscheinen im Kopf der PDF-Ausgaben (#025) + +### Geändert +- Steuersatz, Zahlungsziel und Aufschlag kommen aus den Einstellungen statt aus fest eingetragenen 19 % (#025) + ## [0.15.0] – 2026-09-18 ### Hinzugefügt diff --git a/DEVLOG.md b/DEVLOG.md index 0430020..ec3626b 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -2,7 +2,7 @@ ## Zeitübersicht -**Gesamt:** 10 h 28 min +**Gesamt:** 11 h 38 min | Issue | Zeit | |---|---| @@ -30,6 +30,7 @@ | #022 Cluster-Abgleich | 0 h 25 min | | #023 Feldtest PBS | 0 h 35 min | | #024 OWASP-Härtung | 0 h 45 min | +| #025 Einstellungen | 1 h 10 min | | Meilenstein | Zeit | |---|---| @@ -52,7 +53,7 @@ ## 2026-09-17 · Sitzung 1 · 19:36–laufend **Issue:** #001–#007 (Meilensteine v0.1.0 bis v0.4.0) -**Version:** 0.15.0 +**Version:** 0.16.0 ### Erledigt - Auftrag gelesen, Rückfragen gestellt @@ -142,6 +143,10 @@ - Durchsicht nach OWASP (#024): drei echte Lücken gefunden – Server Actions ohne eigene Zugriffsprüfung, Login-CSRF und fehlende CSP. Alle behoben und mit Tests abgesichert - Erkenntnis: Der Schutz lag vollständig in einer einzigen Schicht (`proxy.ts`). Das fällt erst auf, wenn man die Endpunkte einzeln betrachtet statt die Oberfläche +- Einstellungen im Dashboard (#025): vier Bereiche, alle Werte wirken auch wirklich – der Melderhythmus zum Beispiel bis in den erzeugten systemd-Timer hinein, live geprüft +- Dabei die OIDC-Lücke aus #024 geschlossen: Domänenliste und Schalter gegen automatisch angelegte Konten +- Bewusst nicht gebaut: „stumm nach X Tagen“ (steht schon bei den Warnregeln) und die Währung in der Browseransicht + ### Nächster Schritt - #016 Benutzerhandbuch mit Screenshots, danach #017 Favicons und #019 CI-Pipeline - Für 1.0.0 fehlt noch: PMG (kein Host verfügbar) und der Fall „aktive Subscription auf PBS“ (#023) diff --git a/agent/install.sh b/agent/install.sh index 2db067e..f2febe1 100755 --- a/agent/install.sh +++ b/agent/install.sh @@ -16,9 +16,17 @@ set -o nounset set -o pipefail DASHBOARD_URL="${PROXLEDGER_URL:-@@DASHBOARD_URL@@}" +# Melderhythmus und Streuung setzt das Dashboard beim Ausliefern ein (Einstellungen → System). +# Wird das Skript unverändert aus dem Quelltext ausgeführt, bleiben die Platzhalter stehen; +# dann gelten die Vorgaben. +TIMER_ONCALENDAR="@@TIMER_ONCALENDAR@@" +TIMER_STREUUNG="@@TIMER_STREUUNG@@" +case "$TIMER_ONCALENDAR" in *@@*) TIMER_ONCALENDAR="daily" ;; esac +case "$TIMER_STREUUNG" in *@@*) TIMER_STREUUNG="4h" ;; esac TOKEN="" CA_CERT="" -AUTO_UPDATE="no" +AUTO_UPDATE="@@AUTO_UPDATE@@" +case "$AUTO_UPDATE" in yes | no) ;; *) AUTO_UPDATE="no" ;; esac AKTION="install" CONFIG_VERZEICHNIS="/etc/proxledger" @@ -202,14 +210,14 @@ ProtectSystem=full ProtectHome=true UNIT -cat >"${SYSTEMD_VERZEICHNIS}/proxledger.timer" <<'UNIT' +cat >"${SYSTEMD_VERZEICHNIS}/proxledger.timer" </dev/null -erfolg "systemd-Timer aktiviert (täglich, zufällig um bis zu 4 Stunden versetzt)." +erfolg "systemd-Timer aktiviert (${TIMER_ONCALENDAR}, zufällig um bis zu ${TIMER_STREUUNG} versetzt)." hinweis "Sende ersten Report …" if "$AGENT_PFAD" --report; then diff --git a/docs/anleitung/README.md b/docs/anleitung/README.md index 913ac4b..238eabc 100644 --- a/docs/anleitung/README.md +++ b/docs/anleitung/README.md @@ -17,6 +17,7 @@ auch mit Hosts hinter NAT. 7. [Warnungen](#7-warnungen) 8. [Benachrichtigungen einrichten](#8-benachrichtigungen-einrichten) 9. [Exporte](#9-exporte) +9a. [Einstellungen](#9a-einstellungen) 10. [Sicherung und Aktualisierung](#10-sicherung-und-aktualisierung) 11. [Fehlersuche](#11-fehlersuche) @@ -310,6 +311,36 @@ Jahresliste für die Buchhaltung. --- +## 9a. Einstellungen + +Unter *Einstellungen → System* steht alles, was sich im laufenden Betrieb ändern lässt: + +![Systemeinstellungen](bilder/system.png) + +**Adresse und Agent.** Die Dashboard-Adresse landet im Installations-Einzeiler und in den +Benachrichtigungen. Bleibt das Feld leer, gilt `APP_URL` aus der Umgebung. Melderhythmus, Streuung und +Selbstaktualisierung gelten für **neu installierte** Agents – bestehende übernehmen sie erst, wenn der +Einzeiler dort erneut läuft. + +**Kaufmännisches.** Steuersatz, Zahlungsziel und Aufschlag füllen leere Rechnungsfelder vor. Der +Aufschlag rechnet aus dem Einkaufspreis einen Vorschlag für den Verkaufspreis; eingetragene Beträge +bleiben unberührt. + +**Sicherheit.** Sitzungsdauer und Anmeldeversuche gelten ab der nächsten Anmeldung. Die beiden +OIDC-Felder sind wichtig, wenn ein Identitätsanbieter viele Konten kennt: Ohne Einschränkung darf +jeder, der sich dort anmelden kann, hier mitarbeiten. Mit einer Domänenliste oder abgeschaltetem +„Konten anlegen“ kommt nur herein, wer hier vorgesehen ist. Die Aufbewahrungsfristen räumen nachts um +halb vier auf; der jüngste Report jedes Hosts bleibt immer erhalten, sonst stünde ein lange stummer +Host plötzlich ohne Angaben da. + +**Firma und Darstellung.** Name, Anschrift und E-Mail stehen im Kopf der erzeugten PDF-Dateien, die +Zeitzone gilt für PDF, CSV und Benachrichtigungen. + +> `APP_SECRET` und `DATABASE_URL` stehen bewusst **nicht** hier: Beide werden gebraucht, bevor die +> Anwendung die Datenbank überhaupt lesen kann. + +--- + ## 10. Sicherung und Aktualisierung **Sichern:** diff --git a/docs/anleitung/bilder/host-detail.png b/docs/anleitung/bilder/host-detail.png index 000bf3a..11d27dc 100644 Binary files a/docs/anleitung/bilder/host-detail.png and b/docs/anleitung/bilder/host-detail.png differ diff --git a/docs/anleitung/bilder/kunden.png b/docs/anleitung/bilder/kunden.png index 8574bbc..c8eb81e 100644 Binary files a/docs/anleitung/bilder/kunden.png and b/docs/anleitung/bilder/kunden.png differ diff --git a/docs/anleitung/bilder/login.png b/docs/anleitung/bilder/login.png index 08eb0c3..c82c561 100644 Binary files a/docs/anleitung/bilder/login.png and b/docs/anleitung/bilder/login.png differ diff --git a/docs/anleitung/bilder/nicht-zugeordnet.png b/docs/anleitung/bilder/nicht-zugeordnet.png index 21c54f8..636ffd1 100644 Binary files a/docs/anleitung/bilder/nicht-zugeordnet.png and b/docs/anleitung/bilder/nicht-zugeordnet.png differ diff --git a/docs/anleitung/bilder/rechnungen.png b/docs/anleitung/bilder/rechnungen.png index ee6ebef..b410355 100644 Binary files a/docs/anleitung/bilder/rechnungen.png and b/docs/anleitung/bilder/rechnungen.png differ diff --git a/docs/anleitung/bilder/system.png b/docs/anleitung/bilder/system.png new file mode 100644 index 0000000..a30d3ff Binary files /dev/null and b/docs/anleitung/bilder/system.png differ diff --git a/docs/anleitung/bilder/tokens.png b/docs/anleitung/bilder/tokens.png index 1599c07..bb0e3f2 100644 Binary files a/docs/anleitung/bilder/tokens.png and b/docs/anleitung/bilder/tokens.png differ diff --git a/docs/issues/025-einstellungen.md b/docs/issues/025-einstellungen.md new file mode 100644 index 0000000..d302290 --- /dev/null +++ b/docs/issues/025-einstellungen.md @@ -0,0 +1,47 @@ +# #025 Einstellungen im Dashboard +**Status:** erledigt +**Meilenstein:** v0.16.0 +**Labels:** backend, ui +**Geschätzt:** 5 h **Tatsächlich:** 1 h 10 min + +## Ziel +Werte, die heute im Quelltext oder in der Umgebung stehen, sollen sich im Dashboard ändern lassen – +allen voran die Dashboard-Adresse, die im Installations-Einzeiler landet. + +## Aufgaben +- [x] Typisierter Einstellungsdienst mit Vorgaben und Prüfung (zod), alles in einer JSON-Zeile +- [x] Oberfläche unter *Einstellungen → System* mit vier Bereichen +- [x] **Adresse und Agent:** Dashboard-Adresse, Melderhythmus, Streuung, Selbstaktualisierung +- [x] **Kaufmännisches:** Währung, Steuersatz, Zahlungsziel, Aufschlag +- [x] **Sicherheit:** Sitzungsdauer, Anmeldeversuche, OIDC-Domänen, Konten anlegen, Aufbewahrungsfristen +- [x] **Firma und Darstellung:** Name, Anschrift, E-Mail, Zeitzone +- [x] Werte wirksam machen (nicht nur speichern) +- [x] Nächtliches Aufräumen nach den Aufbewahrungsfristen +- [x] Tests und Handbuchkapitel + +## Wo die Werte wirken +| Einstellung | Wirkt in | +|---|---| +| Dashboard-Adresse | `/install.sh`, `/agent.sh`, Benachrichtigungen | +| Rhythmus, Streuung, Selbstaktualisierung | erzeugte systemd-Timer und `agent.conf` | +| Steuersatz | neue Rechnungspositionen (Einkauf und Verkauf) | +| Zahlungsziel | vorgeschlagenes Fälligkeitsdatum | +| Aufschlag | vorgeschlagener Verkaufspreis aus dem Einkaufspreis | +| Sitzungsdauer | Gültigkeit des Tokens und Lebensdauer des Cookies | +| Anmeldeversuche | Ratenbremse der Anmeldung | +| OIDC-Domänen, Konten anlegen | `mitOidcAnmelden` – schließt die in #024 vermerkte Lücke | +| Aufbewahrungsfristen | nächtlicher Lauf im Worker (03:30) | +| Firma, Zeitzone | Kopf und Datum der PDF-Ausgaben | + +## Entscheidungen +- **Eine JSON-Zeile statt vieler Schlüssel:** Speichern ist ein Schreibvorgang, fehlende Felder + bekommen ihren Vorgabewert, und eine kaputte Zeile legt den Betrieb nicht lahm – dann gelten die Vorgaben. +- **Zehn Sekunden Zwischenspeicher:** Die Werte werden bei fast jeder Anfrage gebraucht, ändern sich + aber selten. Eine Änderung ist trotzdem sofort spürbar. +- **`APP_SECRET` und `DATABASE_URL` bleiben in der Umgebung**; beide werden vor dem ersten + Datenbankzugriff gebraucht. +- **„Stumm nach X Tagen“ bewusst weggelassen:** Das steht schon als Schwellwert bei den Warnregeln. + Zwei Stellen für denselben Wert wären eine Fehlerquelle. +- **Währung und Zeitzone wirken vorerst nur serverseitig** (PDF, CSV, Benachrichtigungen). Die + Browseransicht rechnet weiterhin fest in Euro und Europe/Berlin – das nachzuziehen heißt, die + Formatierung durch alle Client-Komponenten zu reichen, und gehört in einen eigenen Schritt. diff --git a/docs/issues/README.md b/docs/issues/README.md index 2f5c226..307878c 100644 --- a/docs/issues/README.md +++ b/docs/issues/README.md @@ -28,4 +28,5 @@ Details und Architektur: [../architektur.md](../architektur.md). Zeiten werden a | [#022](022-cluster-abgleich.md) | Cluster-Abgleich | erledigt | v0.14.0 | 3 h | 0 h 25 min | | [#023](023-feldtest-pbs-pmg.md) | Feldtest auf PBS und PMG | offen | v1.0.0 | 2 h | 0 h 35 min (PBS erledigt, PMG offen) | | [#024](024-owasp-haertung.md) | Härtung nach OWASP | erledigt | v0.15.0 | 4 h | 0 h 45 min | -| | **Summe** | | | **125 h** | **10 h 28 min** | +| [#025](025-einstellungen.md) | Einstellungen im Dashboard | erledigt | v0.16.0 | 5 h | 1 h 10 min | +| | **Summe** | | | **130 h** | **11 h 38 min** | diff --git a/package.json b/package.json index ec05235..01e3efe 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "proxledger", - "version": "0.15.0", + "version": "0.16.0", "private": true, "scripts": { "dev": "next dev", diff --git a/scripts/screenshots.mjs b/scripts/screenshots.mjs index 51c8988..9d2d5f5 100644 --- a/scripts/screenshots.mjs +++ b/scripts/screenshots.mjs @@ -48,6 +48,7 @@ const SEITEN = [ { name: "tokens", pfad: "/einstellungen/tokens", titel: "Agent-Tokens" }, { name: "benachrichtigungen", pfad: "/einstellungen/benachrichtigungen", titel: "Benachrichtigungen" }, { name: "audit", pfad: "/einstellungen/audit", titel: "Protokoll" }, + { name: "system", pfad: "/einstellungen/system", titel: "Systemeinstellungen" }, ]; async function sitzungsToken() { diff --git a/src/app/(dashboard)/einstellungen/system/page.tsx b/src/app/(dashboard)/einstellungen/system/page.tsx index 7dfbe2d..6f6f4b1 100644 --- a/src/app/(dashboard)/einstellungen/system/page.tsx +++ b/src/app/(dashboard)/einstellungen/system/page.tsx @@ -1,23 +1,32 @@ +import { SystemFormular } from "@/components/einstellungen/SystemFormular"; import { ContentHeader } from "@/components/layout/ContentHeader"; import { Card } from "@/components/ui/Card"; import { APP_VERSION } from "@/lib/version"; +import { einstellungen } from "@/server/einstellungen"; export const metadata = { title: "System" }; -export default function SystemSeite() { +export default async function SystemSeite() { + const werte = await einstellungen(); + return ( <>
- + + +
Version
{APP_VERSION}
-
Datenquelle
-
Beispieldaten (Prototyp, #002)
-
Warnregeln
-
Konfigurierbar ab #011
+
Zeitzone des Servers
+
{Intl.DateTimeFormat().resolvedOptions().timeZone}
+
Nur in der Umgebung
+
+ APP_SECRET und DATABASE_URL – beide werden gebraucht, bevor + die Datenbank gelesen werden kann. +
diff --git a/src/app/(dashboard)/hosts/export/route.ts b/src/app/(dashboard)/hosts/export/route.ts index ae3b9ed..dff02c3 100644 --- a/src/app/(dashboard)/hosts/export/route.ts +++ b/src/app/(dashboard)/hosts/export/route.ts @@ -3,6 +3,7 @@ import { csvAntwort, csvDatei } from "@/lib/csv"; import { formatDatum } from "@/lib/format"; import { Bericht, pdfAntwort, Tabelle } from "@/server/pdf/dokument"; import { hostListe, type HostFilter } from "@/server/queries/hostliste"; +import { einstellungen } from "@/server/einstellungen"; type ExportHost = Awaited>["hosts"][number]; @@ -34,8 +35,12 @@ export async function GET(request: Request) { const datum = new Date().toISOString().slice(0, 10); if (new URL(request.url).searchParams.get("format") === "pdf") { + const { firmaName, firmaAnschrift, firmaEmail, zeitzone } = await einstellungen(); + const firmaAngaben = { name: firmaName, anschrift: firmaAnschrift, email: firmaEmail, zeitzone }; + return pdfAntwort( Bericht({ + firma: firmaAngaben, titel: "Host-Liste", untertitel: `${alle.length} Hosts`, quer: true, diff --git a/src/app/(dashboard)/kunden/[id]/export/route.ts b/src/app/(dashboard)/kunden/[id]/export/route.ts index e4ad1b3..472bc12 100644 --- a/src/app/(dashboard)/kunden/[id]/export/route.ts +++ b/src/app/(dashboard)/kunden/[id]/export/route.ts @@ -2,6 +2,7 @@ import { PRODUKT_TEXT, STATUS_TEXT, statusAusDatenbank } from "@/lib/anzeige"; import { formatDatum, formatEuro } from "@/lib/format"; import { Abschnitt, Bericht, pdfAntwort, Tabelle } from "@/server/pdf/dokument"; import { kundeMitDetails, laufzeitenVonKunde, summen, summenProJahr } from "@/server/queries/kunden"; +import { einstellungen } from "@/server/einstellungen"; export const dynamic = "force-dynamic"; @@ -15,9 +16,12 @@ export async function GET(_request: Request, { params }: { params: Promise<{ id: const gesamt = summen(laufzeiten); const proJahr = summenProJahr(laufzeiten); const datum = new Date().toISOString().slice(0, 10); + const { firmaName, firmaAnschrift, firmaEmail, zeitzone } = await einstellungen(); + const firmaAngaben = { name: firmaName, anschrift: firmaAnschrift, email: firmaEmail, zeitzone }; return pdfAntwort( Bericht({ + firma: firmaAngaben, titel: kunde.name, untertitel: `Kundennummer ${kunde.kundennummer}${kunde.ansprechpartner ? ` · ${kunde.ansprechpartner}` : ""}`, children: [ diff --git a/src/app/(dashboard)/kunden/export/route.ts b/src/app/(dashboard)/kunden/export/route.ts index 85ab442..9d6ebe0 100644 --- a/src/app/(dashboard)/kunden/export/route.ts +++ b/src/app/(dashboard)/kunden/export/route.ts @@ -1,6 +1,7 @@ import { csvAntwort, csvDatei } from "@/lib/csv"; import { formatDatum, formatEuro } from "@/lib/format"; import { Bericht, pdfAntwort, Tabelle } from "@/server/pdf/dokument"; +import { einstellungen } from "@/server/einstellungen"; import { kundenListe, offeneRechnungspunkteProKunde, @@ -30,8 +31,12 @@ export async function GET(request: Request) { const datum = new Date().toISOString().slice(0, 10); if (format === "pdf") { + const { firmaName, firmaAnschrift, firmaEmail, zeitzone } = await einstellungen(); + const firmaAngaben = { name: firmaName, anschrift: firmaAnschrift, email: firmaEmail, zeitzone }; + return pdfAntwort( Bericht({ + firma: firmaAngaben, titel: "Kundenübersicht", untertitel: `${zeilen.length} Kunden`, quer: true, diff --git a/src/app/(dashboard)/rechnungen/[id]/page.tsx b/src/app/(dashboard)/rechnungen/[id]/page.tsx index 347df8b..75f2f12 100644 --- a/src/app/(dashboard)/rechnungen/[id]/page.tsx +++ b/src/app/(dashboard)/rechnungen/[id]/page.tsx @@ -9,6 +9,7 @@ import { CopyableCode } from "@/components/ui/CopyableCode"; import { PRODUKT_TEXT } from "@/lib/anzeige"; import { formatDatum, formatEuro } from "@/lib/format"; import { laufzeitBestaetigenAktion, laufzeitVerwerfenAktion } from "@/server/actions/rechnungen"; +import { einstellungen, faelligkeitsVorschlag } from "@/server/einstellungen"; import { laufzeitDetail } from "@/server/queries/rechnungen"; export async function generateMetadata({ params }: { params: Promise<{ id: string }> }) { @@ -28,6 +29,12 @@ export default async function LaufzeitSeite({ params }: { params: Promise<{ id: const verkaufBetrag = laufzeit.kundenPosition ? Number(laufzeit.kundenPosition.nettoBetrag) : null; const marge = einkaufBetrag !== null && verkaufBetrag !== null ? verkaufBetrag - einkaufBetrag : null; + // Vorgaben aus den Einstellungen füllen leere Felder vor (#025). + const [vorgaben, faelligVorschlag] = await Promise.all([einstellungen(), faelligkeitsVorschlag()]); + const verkaufVorschlag = + verkaufBetrag ?? + (einkaufBetrag === null ? null : Math.round(einkaufBetrag * (1 + vorgaben.margeProzent / 100) * 100) / 100); + const zeitraum = `${formatDatum(laufzeit.start)} – ${formatDatum(laufzeit.ende)}`; return ( @@ -148,9 +155,9 @@ export default async function LaufzeitSeite({ params }: { params: Promise<{ id: werte={{ nummer: einkauf?.nummer ?? null, datum: einkauf?.datum.toISOString() ?? null, - faelligAm: einkauf?.faelligAm?.toISOString() ?? null, + faelligAm: einkauf?.faelligAm?.toISOString() ?? faelligVorschlag, nettoBetrag: einkaufBetrag, - ustSatz: Number(einkauf?.ustSatz ?? 19), + ustSatz: Number(einkauf?.ustSatz ?? vorgaben.steuersatz), rabatt: einkauf?.rabatt === null || einkauf?.rabatt === undefined ? null @@ -179,8 +186,8 @@ export default async function LaufzeitSeite({ params }: { params: Promise<{ id: erstellt: verkauf?.erstellt ?? false, nummer: verkauf?.nummer ?? null, datum: verkauf?.datum?.toISOString() ?? null, - nettoBetrag: verkaufBetrag, - ustSatz: Number(verkauf?.ustSatz ?? 19), + nettoBetrag: verkaufVorschlag, + ustSatz: Number(verkauf?.ustSatz ?? vorgaben.steuersatz), bezahlt: verkauf?.bezahlt ?? false, bezahltAm: verkauf?.bezahltAm?.toISOString() ?? null, anhangId: verkauf?.anhangId ?? null, diff --git a/src/app/(dashboard)/rechnungen/export/route.ts b/src/app/(dashboard)/rechnungen/export/route.ts index 9709560..37d0789 100644 --- a/src/app/(dashboard)/rechnungen/export/route.ts +++ b/src/app/(dashboard)/rechnungen/export/route.ts @@ -2,6 +2,7 @@ import { csvAntwort, csvDatei } from "@/lib/csv"; import { formatDatum, formatEuro } from "@/lib/format"; import { Bericht, pdfAntwort, Tabelle } from "@/server/pdf/dokument"; import { rechnungsListe, type RechnungsFilter } from "@/server/queries/rechnungen"; +import { einstellungen } from "@/server/einstellungen"; export const dynamic = "force-dynamic"; @@ -21,8 +22,12 @@ export async function GET(request: Request) { const zeitraum = filter.jahr ? `Jahr ${filter.jahr}` : "alle Jahre"; if (format === "pdf") { + const { firmaName, firmaAnschrift, firmaEmail, zeitzone } = await einstellungen(); + const firmaAngaben = { name: firmaName, anschrift: firmaAnschrift, email: firmaEmail, zeitzone }; + return pdfAntwort( Bericht({ + firma: firmaAngaben, titel: "Rechnungsliste", untertitel: `${zeilen.length} Laufzeiten · ${zeitraum}`, quer: true, diff --git a/src/app/agent.sh/route.ts b/src/app/agent.sh/route.ts index fb34265..948739c 100644 --- a/src/app/agent.sh/route.ts +++ b/src/app/agent.sh/route.ts @@ -4,6 +4,6 @@ export const dynamic = "force-dynamic"; /** Aktuelles Agent-Skript, wird vom Installer und vom Update-Mechanismus geladen. */ export async function GET(request: Request) { - const inhalt = await skriptLaden("proxledger-agent.sh", basisUrl(request)); + const inhalt = await skriptLaden("proxledger-agent.sh", await basisUrl(request)); return skriptAntwort(inhalt, "proxledger-agent.sh"); } diff --git a/src/app/api/auth/anmelden/route.ts b/src/app/api/auth/anmelden/route.ts index ccceb97..28a4044 100644 --- a/src/app/api/auth/anmelden/route.ts +++ b/src/app/api/auth/anmelden/route.ts @@ -2,6 +2,7 @@ import { cookies, headers } from "next/headers"; import { clientIp } from "@/lib/ratelimit"; import { cookieOptionen, SITZUNGS_COOKIE, sitzungErzeugen } from "@/lib/sitzung"; import { AnmeldeFehler, mitPasswortAnmelden } from "@/server/auth/benutzer"; +import { einstellungen } from "@/server/einstellungen"; export const dynamic = "force-dynamic"; @@ -62,13 +63,17 @@ export async function POST(request: Request) { const kopfzeilen = await headers(); const benutzer = await mitPasswortAnmelden(email, passwort, clientIp(kopfzeilen)); - const token = await sitzungErzeugen({ - benutzerId: benutzer.id, - email: benutzer.email, - name: benutzer.name, - herkunft: "passwort", - }); - (await cookies()).set(SITZUNGS_COOKIE, token, cookieOptionen()); + const { sitzungsdauerStunden } = await einstellungen(); + const token = await sitzungErzeugen( + { + benutzerId: benutzer.id, + email: benutzer.email, + name: benutzer.name, + herkunft: "passwort", + }, + sitzungsdauerStunden, + ); + (await cookies()).set(SITZUNGS_COOKIE, token, cookieOptionen(sitzungsdauerStunden)); return Response.redirect(new URL(ziel, request.url), 303); } catch (fehler) { diff --git a/src/app/api/auth/oidc/callback/route.ts b/src/app/api/auth/oidc/callback/route.ts index e4f7e0e..bd369fc 100644 --- a/src/app/api/auth/oidc/callback/route.ts +++ b/src/app/api/auth/oidc/callback/route.ts @@ -2,6 +2,7 @@ import { cookies } from "next/headers"; import { cookieOptionen, SITZUNGS_COOKIE, sitzungErzeugen } from "@/lib/sitzung"; import { mitOidcAnmelden } from "@/server/auth/benutzer"; import { anmeldungAbschliessen, oidcEinstellungen } from "@/server/auth/oidc"; +import { einstellungen as systemEinstellungen } from "@/server/einstellungen"; export const dynamic = "force-dynamic"; @@ -25,13 +26,17 @@ export async function GET(request: Request) { const angaben = await anmeldungAbschliessen(einstellungen, new URL(request.url), pruefwert, zustand); const benutzer = await mitOidcAnmelden(angaben.subject, angaben.email, angaben.name); - const token = await sitzungErzeugen({ - benutzerId: benutzer.id, - email: benutzer.email, - name: benutzer.name, - herkunft: "oidc", - }); - speicher.set(SITZUNGS_COOKIE, token, cookieOptionen()); + const { sitzungsdauerStunden } = await systemEinstellungen(); + const token = await sitzungErzeugen( + { + benutzerId: benutzer.id, + email: benutzer.email, + name: benutzer.name, + herkunft: "oidc", + }, + sitzungsdauerStunden, + ); + speicher.set(SITZUNGS_COOKIE, token, cookieOptionen(sitzungsdauerStunden)); return Response.redirect(new URL(ziel.startsWith("/") ? ziel : "/", request.url), 302); } catch (fehler) { diff --git a/src/app/install.sh/route.ts b/src/app/install.sh/route.ts index 39e5ee2..1490b2e 100644 --- a/src/app/install.sh/route.ts +++ b/src/app/install.sh/route.ts @@ -4,6 +4,6 @@ export const dynamic = "force-dynamic"; /** Installationsskript für den One-Liner `curl -fsSL /install.sh | bash -s -- --token …`. */ export async function GET(request: Request) { - const inhalt = await skriptLaden("install.sh", basisUrl(request)); + const inhalt = await skriptLaden("install.sh", await basisUrl(request)); return skriptAntwort(inhalt, "install.sh"); } diff --git a/src/components/einstellungen/SystemFormular.tsx b/src/components/einstellungen/SystemFormular.tsx new file mode 100644 index 0000000..2d8b3b6 --- /dev/null +++ b/src/components/einstellungen/SystemFormular.tsx @@ -0,0 +1,341 @@ +"use client"; + +import { useActionState } from "react"; +import { Card } from "@/components/ui/Card"; +import { type AktionsErgebnis, einstellungenSpeichernAktion } from "@/server/actions/einstellungen"; +import type { Einstellungen } from "@/server/einstellungen"; + +const LEER: AktionsErgebnis = {}; + +function Feld({ + name, + beschriftung, + hinweis, + fehler, + children, +}: { + name: string; + beschriftung: string; + hinweis?: string; + fehler?: string; + children: React.ReactNode; +}) { + return ( +
+ + {children} + {hinweis ?
{hinweis}
: null} + {fehler ?
{fehler}
: null} +
+ ); +} + +export function SystemFormular({ werte, appUrl }: { werte: Einstellungen; appUrl: string }) { + const [ergebnis, aktion, laeuft] = useActionState(einstellungenSpeichernAktion, LEER); + const fehler = ergebnis.felder ?? {}; + + return ( +
+ {ergebnis.fehler ?
{ergebnis.fehler}
: null} + {ergebnis.erfolg ?
{ergebnis.erfolg}
: null} + +
+
+ + + + + + + + + + + + + +
+ + +
+ Der Agent lädt bei einer neueren Fassung das Skript vom Dashboard nach. +
+
+
+
+ +
+ + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ + +
+ Aus heißt: Nur wer hier schon ein Konto hat, kommt herein – auch wenn die Anmeldung + beim Identitätsanbieter klappt. +
+
+ + + + + + + + +
+
+ +
+ + + + + + +