Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d8e5c9172 | ||
|
|
6c3729bae8 |
+5
-1
@@ -24,7 +24,8 @@ OIDC_CLIENT_ID=""
|
|||||||
OIDC_CLIENT_SECRET=""
|
OIDC_CLIENT_SECRET=""
|
||||||
OIDC_NAME="authentik"
|
OIDC_NAME="authentik"
|
||||||
|
|
||||||
# Öffentlich erreichbare Adresse des Dashboards (für install.sh und E-Mails)
|
# Öffentlich erreichbare Adresse des Dashboards (für install.sh und E-Mails).
|
||||||
|
# Lässt sich später unter Einstellungen → System überschreiben.
|
||||||
APP_URL="http://localhost:3000"
|
APP_URL="http://localhost:3000"
|
||||||
|
|
||||||
# Zeitplan des Workers (Standard: stündlich zur vollen Stunde)
|
# Zeitplan des Workers (Standard: stündlich zur vollen Stunde)
|
||||||
@@ -33,6 +34,9 @@ WORKER_CRON="0 * * * *"
|
|||||||
# Zeitplan der Zusammenfassung (Standard: täglich 07:00 Uhr)
|
# Zeitplan der Zusammenfassung (Standard: täglich 07:00 Uhr)
|
||||||
ZUSAMMENFASSUNG_CRON="0 7 * * *"
|
ZUSAMMENFASSUNG_CRON="0 7 * * *"
|
||||||
|
|
||||||
|
# Zeitplan fürs Aufräumen alter Reports und Protokolleinträge (Fristen: Einstellungen → System)
|
||||||
|
AUFRAEUM_CRON="30 3 * * *"
|
||||||
|
|
||||||
# Ablage für hochgeladene Rechnungs-PDFs
|
# Ablage für hochgeladene Rechnungs-PDFs
|
||||||
UPLOAD_DIR="./data/uploads"
|
UPLOAD_DIR="./data/uploads"
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,20 @@ und dieses Projekt folgt [Semantic Versioning](https://semver.org/lang/de/).
|
|||||||
|
|
||||||
### Sicherheit
|
### Sicherheit
|
||||||
|
|
||||||
|
## [0.17.0] – 2026-09-19
|
||||||
|
|
||||||
|
### Hinzugefügt
|
||||||
|
- `deploy/update.sh`: sichert, baut neu, migriert und prüft, ob das Dashboard wieder antwortet (#026)
|
||||||
|
- Installationsskripte nach Art der Proxmox VE Helper-Scripts unter `deploy/community-scripts/` – Container mit Node und PostgreSQL, ohne Docker (#026)
|
||||||
|
- `docs/roadmap.md`: was bis 1.0.0 fehlt und was danach lohnt (#026)
|
||||||
|
- Zustandsprüfung für den Worker im Docker-Stack (#026)
|
||||||
|
|
||||||
|
### Behoben
|
||||||
|
- Nach der Anmeldung führte die Weiterleitung im Container auf den internen Containernamen (`http://<container-id>:3000`) statt auf die erreichbare Adresse – hinter einem Reverse Proxy landete man damit im Nichts. Beim Prüfen des Docker-Stapels aufgefallen (#026)
|
||||||
|
|
||||||
|
### Geändert
|
||||||
|
- README auf den tatsächlichen Stand gebracht: Der Feldtest ist erledigt, der Hinweis „noch nicht erprobt“ war überholt (#026)
|
||||||
|
|
||||||
## [0.16.0] – 2026-09-18
|
## [0.16.0] – 2026-09-18
|
||||||
|
|
||||||
### Hinzugefügt
|
### Hinzugefügt
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Zeitübersicht
|
## Zeitübersicht
|
||||||
|
|
||||||
**Gesamt:** 11 h 38 min
|
**Gesamt:** 12 h 18 min
|
||||||
|
|
||||||
| Issue | Zeit |
|
| Issue | Zeit |
|
||||||
|---|---|
|
|---|---|
|
||||||
@@ -31,6 +31,7 @@
|
|||||||
| #023 Feldtest PBS | 0 h 35 min |
|
| #023 Feldtest PBS | 0 h 35 min |
|
||||||
| #024 OWASP-Härtung | 0 h 45 min |
|
| #024 OWASP-Härtung | 0 h 45 min |
|
||||||
| #025 Einstellungen | 1 h 10 min |
|
| #025 Einstellungen | 1 h 10 min |
|
||||||
|
| #026 Auslieferung | 0 h 40 min |
|
||||||
|
|
||||||
| Meilenstein | Zeit |
|
| Meilenstein | Zeit |
|
||||||
|---|---|
|
|---|---|
|
||||||
@@ -53,7 +54,7 @@
|
|||||||
|
|
||||||
## 2026-09-17 · Sitzung 1 · 19:36–laufend
|
## 2026-09-17 · Sitzung 1 · 19:36–laufend
|
||||||
**Issue:** #001–#007 (Meilensteine v0.1.0 bis v0.4.0)
|
**Issue:** #001–#007 (Meilensteine v0.1.0 bis v0.4.0)
|
||||||
**Version:** 0.16.0
|
**Version:** 0.17.0
|
||||||
|
|
||||||
### Erledigt
|
### Erledigt
|
||||||
- Auftrag gelesen, Rückfragen gestellt
|
- Auftrag gelesen, Rückfragen gestellt
|
||||||
@@ -147,6 +148,10 @@
|
|||||||
- Dabei die OIDC-Lücke aus #024 geschlossen: Domänenliste und Schalter gegen automatisch angelegte Konten
|
- 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
|
- Bewusst nicht gebaut: „stumm nach X Tagen“ (steht schon bei den Warnregeln) und die Währung in der Browseransicht
|
||||||
|
|
||||||
|
- Auslieferung finalisiert (#026): Docker-Weg mit Update-Skript, dazu Skripte nach Art der Helper-Scripts für einen LXC ohne Docker
|
||||||
|
- Recherche dazu: Das Gerüst der community-scripts liegt inzwischen in einem eigenen Repository, und neue Skripte gehören zuerst nach ProxmoxVED. Eine Einreichung setzt öffentlichen Quelltext voraus – deshalb vorerst intern
|
||||||
|
- Roadmap geschrieben: Bis 1.0.0 fehlt kein Code mehr, sondern Erprobung (PMG, PBS mit Lizenz, ein Monat Dauerbetrieb, einmal eine Sicherung zurückspielen)
|
||||||
|
|
||||||
### Nächster Schritt
|
### Nächster Schritt
|
||||||
- #016 Benutzerhandbuch mit Screenshots, danach #017 Favicons und #019 CI-Pipeline
|
- #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)
|
- Für 1.0.0 fehlt noch: PMG (kein Host verfügbar) und der Fall „aktive Subscription auf PBS“ (#023)
|
||||||
|
|||||||
@@ -3,8 +3,10 @@
|
|||||||
Zentrales Dashboard für den Subscription-Status aller Proxmox-Hosts (VE, Backup Server, Mail Gateway).
|
Zentrales Dashboard für den Subscription-Status aller Proxmox-Hosts (VE, Backup Server, Mail Gateway).
|
||||||
Auf jedem Host meldet ein schlanker Agent täglich per HTTPS seinen Status (Push-Modell) – auch aus Kundennetzen hinter NAT.
|
Auf jedem Host meldet ein schlanker Agent täglich per HTTPS seinen Status (Push-Modell) – auch aus Kundennetzen hinter NAT.
|
||||||
|
|
||||||
> **Status:** in Entwicklung. Der Funktionsumfang steht, der Agent wurde aber noch nicht auf echten
|
> **Status:** einsatzbereit. Der Agent ist auf echten Hosts erprobt – Proxmox VE 9.2 mit und ohne
|
||||||
> Proxmox-Hosts erprobt (#018).
|
> Subscription, im Cluster und einzeln, sowie Proxmox Backup Server 4.2 (#018, #023).
|
||||||
|
> Ungeprüft: Mail Gateway und eine aktive Subscription auf PBS. Was bis 1.0 fehlt, steht in
|
||||||
|
> [docs/roadmap.md](docs/roadmap.md).
|
||||||
|
|
||||||
## Schnellstart
|
## Schnellstart
|
||||||
|
|
||||||
@@ -20,14 +22,27 @@ docker compose up -d
|
|||||||
Beim ersten Start legt der Dienst `migrate` das Schema an und erzeugt den Administrator aus der `.env`.
|
Beim ersten Start legt der Dienst `migrate` das Schema an und erzeugt den Administrator aus der `.env`.
|
||||||
Danach ist das Dashboard unter `APP_URL` erreichbar.
|
Danach ist das Dashboard unter `APP_URL` erreichbar.
|
||||||
|
|
||||||
**Auf einem Proxmox-Host** legt der Installer einen fertigen LXC-Container an:
|
Aktualisieren geht mit einem Befehl – er sichert vorher, baut neu, migriert und prüft zum Schluss,
|
||||||
|
ob das Dashboard wieder antwortet:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bash deploy/lxc-installer.sh
|
bash deploy/update.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
Er fragt Ressourcen und Netzwerk ab, installiert Docker, holt den Quelltext, erzeugt Zufallspasswörter
|
**Auf einem Proxmox-Host** gibt es zwei Wege zu einem fertigen LXC-Container:
|
||||||
und startet alles. Am Ende stehen Adresse, Benutzer und Passwort auf dem Bildschirm.
|
|
||||||
|
```bash
|
||||||
|
bash deploy/lxc-installer.sh # eigener Installer: Container mit Docker darin
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Nach Art der Proxmox VE Helper-Scripts: Container mit Node und PostgreSQL direkt darin
|
||||||
|
bash -c "$(curl -fsSL https://<git>/ProxLedger/raw/branch/main/deploy/community-scripts/ct/proxledger.sh)"
|
||||||
|
```
|
||||||
|
|
||||||
|
Der zweite Weg folgt den Konventionen von [community-scripts.org](https://community-scripts.org) und
|
||||||
|
braucht kein Docker; Einzelheiten und die Voraussetzungen für eine Einreichung stehen in
|
||||||
|
[deploy/community-scripts/README.md](deploy/community-scripts/README.md).
|
||||||
|
|
||||||
### Agent ausrollen
|
### Agent ausrollen
|
||||||
|
|
||||||
@@ -105,13 +120,28 @@ Die Oberfläche ist vollständig geschützt; die Agent-API und die Skript-Auslie
|
|||||||
sie sich mit eigenen Tokens ausweisen.
|
sie sich mit eigenen Tokens ausweisen.
|
||||||
|
|
||||||
- **Lokaler Zugang:** E-Mail und Passwort aus dem Seed (`ADMIN_EMAIL`, `ADMIN_PASSWORD`). Fünf Fehlversuche
|
- **Lokaler Zugang:** E-Mail und Passwort aus dem Seed (`ADMIN_EMAIL`, `ADMIN_PASSWORD`). Fünf Fehlversuche
|
||||||
je Kennung und Viertelstunde, danach Sperre.
|
je Kennung und Viertelstunde, danach Sperre (einstellbar unter *Einstellungen → System*).
|
||||||
|
Benutzer sperren oder Passwort setzen: `npm run benutzer -- sperren <email>`.
|
||||||
- **OIDC (z. B. authentik):** `OIDC_ISSUER`, `OIDC_CLIENT_ID`, `OIDC_CLIENT_SECRET` setzen. Rückkehradresse
|
- **OIDC (z. B. authentik):** `OIDC_ISSUER`, `OIDC_CLIENT_ID`, `OIDC_CLIENT_SECRET` setzen. Rückkehradresse
|
||||||
ist `<APP_URL>/api/auth/oidc/callback`. Ohne diese Werte erscheint nur der lokale Zugang – der bleibt
|
ist `<APP_URL>/api/auth/oidc/callback`. Ohne diese Werte erscheint nur der lokale Zugang – der bleibt
|
||||||
absichtlich immer nutzbar, falls der Anmeldedienst ausfällt.
|
absichtlich immer nutzbar, falls der Anmeldedienst ausfällt. Unter *Einstellungen → System* lässt sich
|
||||||
|
einschränken, welche E-Mail-Domänen zugelassen sind und ob unbekannte Konten angelegt werden dürfen.
|
||||||
- **`APP_SECRET`** (mindestens 32 Zeichen) signiert die Sitzungs-Cookies. Ändert sich der Wert, sind alle
|
- **`APP_SECRET`** (mindestens 32 Zeichen) signiert die Sitzungs-Cookies. Ändert sich der Wert, sind alle
|
||||||
Sitzungen ungültig.
|
Sitzungen ungültig.
|
||||||
|
|
||||||
|
## Einstellungen
|
||||||
|
|
||||||
|
Die meisten Betriebswerte stehen unter *Einstellungen → System* und wirken sofort: Dashboard-Adresse,
|
||||||
|
Melderhythmus und Selbstaktualisierung der Agents, Steuersatz, Zahlungsziel und Aufschlag,
|
||||||
|
Sitzungsdauer und Anmeldeversuche, OIDC-Beschränkungen, Aufbewahrungsfristen sowie Firmenangaben und
|
||||||
|
Zeitzone für die PDF-Ausgaben.
|
||||||
|
|
||||||
|
In der Umgebung bleiben nur `APP_SECRET` und `DATABASE_URL` – beide werden gebraucht, bevor die
|
||||||
|
Anwendung die Datenbank lesen kann.
|
||||||
|
|
||||||
|
Wie die Anwendung gegen die OWASP Top 10 dasteht (einschließlich der Punkte, die bewusst offen
|
||||||
|
bleiben), steht in [docs/sicherheit.md](docs/sicherheit.md).
|
||||||
|
|
||||||
## Exporte
|
## Exporte
|
||||||
|
|
||||||
| Ansicht | CSV | PDF |
|
| Ansicht | CSV | PDF |
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
# Installation als LXC-Container nach Art der Proxmox VE Helper-Scripts
|
||||||
|
|
||||||
|
Diese beiden Skripte folgen den Konventionen von [community-scripts.org](https://community-scripts.org)
|
||||||
|
(der Nachfolge von tteck's Helper-Scripts). Sie legen einen unprivilegierten Debian-13-Container an,
|
||||||
|
installieren Node.js 24 und PostgreSQL 17, holen ProxLedger aus dem Git-Repository, richten zwei
|
||||||
|
systemd-Dienste ein (Dashboard und Worker) und schreiben die erzeugten Zugangsdaten nach
|
||||||
|
`/opt/proxledger/.credentials`.
|
||||||
|
|
||||||
|
| Datei | Zweck |
|
||||||
|
|---|---|
|
||||||
|
| `ct/proxledger.sh` | Container anlegen, Ressourcen abfragen, Aktualisierung (`update_script`) |
|
||||||
|
| `install/proxledger-install.sh` | Installation im Container |
|
||||||
|
|
||||||
|
## Verwendung im eigenen Netz
|
||||||
|
|
||||||
|
Auf dem Proxmox-VE-Host als root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash -c "$(curl -fsSL https://git.wilde-server.de/fw/ProxLedger/raw/branch/main/deploy/community-scripts/ct/proxledger.sh)"
|
||||||
|
```
|
||||||
|
|
||||||
|
Das Gerüst (`build.func`, `install.func`) lädt das Skript von den community-scripts nach – das ist
|
||||||
|
öffentlich und ohne Konto erreichbar. Nur ProxLedger selbst kommt aus dem eigenen Git.
|
||||||
|
|
||||||
|
Zwei Umgebungsvariablen steuern die Quelle, solange das Repository nicht öffentlich ist:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PROXLEDGER_REPO=https://git.example.de/fw/ProxLedger.git PROXLEDGER_BRANCH=main bash -c "$(curl -fsSL …)"
|
||||||
|
```
|
||||||
|
|
||||||
|
Ist das Git-Repository nicht ohne Anmeldung lesbar, gehört ein Lesezugriffs-Token in die Adresse
|
||||||
|
(`https://<token>@git.example.de/...`). Das Token landet dann in der Prozessliste des Containers –
|
||||||
|
also besser einen Deploy-Token verwenden, der nur lesen darf.
|
||||||
|
|
||||||
|
## Was eine Einreichung bei community-scripts verlangen würde
|
||||||
|
|
||||||
|
Wir liefern die Skripte vorerst **nur intern** aus. Für eine Einreichung wären zusätzlich nötig:
|
||||||
|
|
||||||
|
1. **Öffentlich erreichbarer Quelltext.** Die Skripte dort holen die Anwendung praktisch immer aus
|
||||||
|
einem öffentlichen GitHub-Repository (`fetch_and_deploy_gh_release`). Ein privates Gitea genügt nicht.
|
||||||
|
2. **Pull Request gegen [ProxmoxVED](https://github.com/community-scripts/ProxmoxVED)**, nicht gegen
|
||||||
|
ProxmoxVE. Neue Skripte, die direkt gegen ProxmoxVE eingereicht werden, werden ohne Prüfung
|
||||||
|
geschlossen.
|
||||||
|
3. **MIT-Lizenz** im eigenen Repository und der Lizenzkopf in beiden Skripten (steht bereits darin).
|
||||||
|
4. **Englische Dokumentation** – die Skripte selbst sind auf Englisch gehalten, die deutschen
|
||||||
|
Kommentare in diesem Verzeichnis müssten übersetzt werden.
|
||||||
|
5. **Metadaten für die Webseite.** Die JSON-Dateien liegen inzwischen nicht mehr im Skript-Repository;
|
||||||
|
wo sie hingehören, steht in der aktuellen Beitragsanleitung. Das ist beim Einreichen zu prüfen.
|
||||||
|
|
||||||
|
Die Skripte sind so geschrieben, dass für eine spätere Einreichung nur die Quelle zu tauschen und die
|
||||||
|
Kommentare zu übersetzen sind – am Ablauf ändert sich nichts.
|
||||||
|
|
||||||
|
## Unterschied zum Docker-Weg
|
||||||
|
|
||||||
|
| | Helper-Script (LXC) | Docker Compose |
|
||||||
|
|---|---|---|
|
||||||
|
| Umgebung | eigener Container auf dem Proxmox-Host | beliebiger Docker-Host |
|
||||||
|
| Datenbank | PostgreSQL im selben Container | eigener Dienst |
|
||||||
|
| Aktualisierung | `update`-Weg des Skripts | `bash deploy/update.sh` |
|
||||||
|
| Sicherung | Proxmox-Sicherung des Containers | `bash deploy/backup.sh` |
|
||||||
|
|
||||||
|
Beide Wege sind gleichwertig. Wer ohnehin Proxmox betreibt, fährt mit dem LXC-Weg meist einfacher –
|
||||||
|
die Sicherung des Containers erledigt dann die gewohnte Proxmox-Sicherung mit.
|
||||||
Executable
+74
@@ -0,0 +1,74 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
_CS_DEFAULT_URL="https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main"
|
||||||
|
_cs_boot="${COMMUNITY_SCRIPTS_CORE_DIR:-$(dirname "${BASH_SOURCE[0]}")/../../core}/core/build.func"
|
||||||
|
source "$_cs_boot" 2>/dev/null || source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/core/build.func")
|
||||||
|
# Copyright (c) 2021-2026 tteck
|
||||||
|
# Author: Florian Wilde
|
||||||
|
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||||
|
# Source: https://git.wilde-server.de/fw/ProxLedger
|
||||||
|
|
||||||
|
APP="ProxLedger"
|
||||||
|
var_tags="${var_tags:-monitoring;licensing}"
|
||||||
|
var_cpu="${var_cpu:-2}"
|
||||||
|
var_ram="${var_ram:-2048}"
|
||||||
|
var_disk="${var_disk:-10}"
|
||||||
|
var_os="${var_os:-debian}"
|
||||||
|
var_version="${var_version:-13}"
|
||||||
|
var_arm64="${var_arm64:-yes}"
|
||||||
|
var_unprivileged="${var_unprivileged:-1}"
|
||||||
|
|
||||||
|
header_info "$APP"
|
||||||
|
variables
|
||||||
|
color
|
||||||
|
catch_errors
|
||||||
|
|
||||||
|
function update_script() {
|
||||||
|
header_info
|
||||||
|
check_container_storage
|
||||||
|
check_container_resources
|
||||||
|
|
||||||
|
if [[ ! -d /opt/proxledger ]]; then
|
||||||
|
msg_error "No ${APP} Installation Found!"
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
|
||||||
|
NODE_VERSION="24" setup_nodejs
|
||||||
|
|
||||||
|
msg_info "Stopping Services"
|
||||||
|
systemctl stop proxledger proxledger-worker
|
||||||
|
msg_ok "Stopped Services"
|
||||||
|
|
||||||
|
# Die .env traegt APP_SECRET; geht sie verloren, sind Sitzungen und gespeicherte
|
||||||
|
# Zugangsdaten unbrauchbar. Deshalb vor jedem Update sichern und danach zurueckspielen.
|
||||||
|
create_backup /opt/proxledger/.env
|
||||||
|
|
||||||
|
msg_info "Updating ${APP}"
|
||||||
|
cd /opt/proxledger
|
||||||
|
$STD git pull --ff-only
|
||||||
|
$STD npm ci
|
||||||
|
$STD npx prisma generate
|
||||||
|
$STD npx prisma migrate deploy
|
||||||
|
$STD npm run build
|
||||||
|
$STD npm run build:worker
|
||||||
|
msg_ok "Updated ${APP}"
|
||||||
|
|
||||||
|
restore_backup
|
||||||
|
|
||||||
|
msg_info "Starting Services"
|
||||||
|
systemctl start proxledger proxledger-worker
|
||||||
|
msg_ok "Started Services"
|
||||||
|
|
||||||
|
msg_ok "Update Successful"
|
||||||
|
exit
|
||||||
|
}
|
||||||
|
|
||||||
|
start
|
||||||
|
build_container
|
||||||
|
description
|
||||||
|
|
||||||
|
msg_ok "Completed Successfully!\n"
|
||||||
|
echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}"
|
||||||
|
echo -e "${INFO}${YW} Access it using the following URL:${CL}"
|
||||||
|
echo -e "${TAB}${GATEWAY}${BGN}http://${IP}:3000${CL}"
|
||||||
|
echo -e "${INFO}${YW} Login credentials are in:${CL}"
|
||||||
|
echo -e "${TAB}${GATEWAY}${BGN}/opt/proxledger/.credentials${CL}"
|
||||||
+127
@@ -0,0 +1,127 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Copyright (c) 2021-2026 tteck
|
||||||
|
# Author: Florian Wilde
|
||||||
|
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||||
|
# Source: https://git.wilde-server.de/fw/ProxLedger
|
||||||
|
|
||||||
|
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
|
||||||
|
color
|
||||||
|
verb_ip6
|
||||||
|
catch_errors
|
||||||
|
setting_up_container
|
||||||
|
network_check
|
||||||
|
update_os
|
||||||
|
|
||||||
|
# Die Quelle laesst sich ueberschreiben, solange das Repository nicht oeffentlich ist.
|
||||||
|
QUELLE="${PROXLEDGER_REPO:-https://git.wilde-server.de/fw/ProxLedger.git}"
|
||||||
|
ZWEIG="${PROXLEDGER_BRANCH:-main}"
|
||||||
|
|
||||||
|
msg_info "Installing Dependencies"
|
||||||
|
$STD apt install -y git ca-certificates
|
||||||
|
msg_ok "Installed Dependencies"
|
||||||
|
|
||||||
|
NODE_VERSION="24" setup_nodejs
|
||||||
|
PG_VERSION="17" setup_postgresql
|
||||||
|
|
||||||
|
msg_info "Setting up Database"
|
||||||
|
DB_NAME="proxledger"
|
||||||
|
DB_USER="proxledger"
|
||||||
|
DB_PASS=$(openssl rand -base64 24 | tr -dc 'A-Za-z0-9' | head -c 24)
|
||||||
|
ADMIN_PASS=$(openssl rand -base64 24 | tr -dc 'A-Za-z0-9' | head -c 20)
|
||||||
|
# APP_SECRET signiert die Sitzungen und verschluesselt gespeicherte Zugangsdaten.
|
||||||
|
APP_SECRET=$(openssl rand -base64 48)
|
||||||
|
|
||||||
|
$STD sudo -u postgres psql -c "CREATE USER ${DB_USER} WITH PASSWORD '${DB_PASS}';"
|
||||||
|
$STD sudo -u postgres psql -c "CREATE DATABASE ${DB_NAME} WITH OWNER ${DB_USER} ENCODING 'UTF8';"
|
||||||
|
msg_ok "Set up Database"
|
||||||
|
|
||||||
|
msg_info "Setting up ProxLedger"
|
||||||
|
$STD git clone --branch "$ZWEIG" --depth 1 "$QUELLE" /opt/proxledger
|
||||||
|
cd /opt/proxledger
|
||||||
|
|
||||||
|
IP_ADRESSE=$(hostname -I | awk '{print $1}')
|
||||||
|
cat <<EOF >/opt/proxledger/.env
|
||||||
|
DATABASE_URL="postgresql://${DB_USER}:${DB_PASS}@localhost:5432/${DB_NAME}?schema=public"
|
||||||
|
APP_SECRET="${APP_SECRET}"
|
||||||
|
APP_URL="http://${IP_ADRESSE}:3000"
|
||||||
|
ADMIN_EMAIL="admin@proxledger.local"
|
||||||
|
ADMIN_NAME="Administrator"
|
||||||
|
ADMIN_PASSWORD="${ADMIN_PASS}"
|
||||||
|
UPLOAD_DIR="/opt/proxledger/data/uploads"
|
||||||
|
NODE_ENV="production"
|
||||||
|
PORT="3000"
|
||||||
|
EOF
|
||||||
|
chmod 600 /opt/proxledger/.env
|
||||||
|
mkdir -p /opt/proxledger/data/uploads
|
||||||
|
|
||||||
|
$STD npm ci
|
||||||
|
$STD npx prisma generate
|
||||||
|
$STD npx prisma migrate deploy
|
||||||
|
$STD npx prisma db seed
|
||||||
|
$STD npm run build
|
||||||
|
$STD npm run build:worker
|
||||||
|
msg_ok "Set up ProxLedger"
|
||||||
|
|
||||||
|
msg_info "Creating Services"
|
||||||
|
cat <<EOF >/etc/systemd/system/proxledger.service
|
||||||
|
[Unit]
|
||||||
|
Description=ProxLedger Dashboard
|
||||||
|
After=network.target postgresql.service
|
||||||
|
Requires=postgresql.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
WorkingDirectory=/opt/proxledger
|
||||||
|
EnvironmentFile=/opt/proxledger/.env
|
||||||
|
ExecStart=/usr/bin/npm run start
|
||||||
|
Restart=always
|
||||||
|
RestartSec=10
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat <<EOF >/etc/systemd/system/proxledger-worker.service
|
||||||
|
[Unit]
|
||||||
|
Description=ProxLedger Worker (Warnregeln und Benachrichtigungen)
|
||||||
|
After=network.target postgresql.service
|
||||||
|
Requires=postgresql.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
WorkingDirectory=/opt/proxledger
|
||||||
|
EnvironmentFile=/opt/proxledger/.env
|
||||||
|
ExecStart=/usr/bin/node dist/worker.mjs
|
||||||
|
Restart=always
|
||||||
|
RestartSec=10
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
systemctl enable -q --now proxledger proxledger-worker
|
||||||
|
msg_ok "Created Services"
|
||||||
|
|
||||||
|
# Die Zugangsdaten liegen fuer den Betreiber lesbar im Container; APP_SECRET steht in der .env.
|
||||||
|
{
|
||||||
|
echo "ProxLedger"
|
||||||
|
echo "Adresse: http://${IP_ADRESSE}:3000"
|
||||||
|
echo "Benutzer: admin@proxledger.local"
|
||||||
|
echo "Passwort: ${ADMIN_PASS}"
|
||||||
|
echo
|
||||||
|
echo "Datenbank: ${DB_NAME} / ${DB_USER} / ${DB_PASS}"
|
||||||
|
echo
|
||||||
|
echo "Wichtig: APP_SECRET aus /opt/proxledger/.env mitsichern."
|
||||||
|
echo "Ohne diesen Wert sind Sitzungen und gespeicherte Zugangsdaten unbrauchbar."
|
||||||
|
} >>~/proxledger.creds
|
||||||
|
cp ~/proxledger.creds /opt/proxledger/.credentials
|
||||||
|
chmod 600 /opt/proxledger/.credentials
|
||||||
|
|
||||||
|
motd_ssh
|
||||||
|
customize
|
||||||
|
|
||||||
|
msg_info "Cleaning up"
|
||||||
|
$STD apt -y autoremove
|
||||||
|
$STD apt -y autoclean
|
||||||
|
msg_ok "Cleaned"
|
||||||
Executable
+60
@@ -0,0 +1,60 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ProxLedger aktualisieren (Docker-Installation).
|
||||||
|
#
|
||||||
|
# bash deploy/update.sh # Quelltext holen, neu bauen, migrieren, neu starten
|
||||||
|
# bash deploy/update.sh --ohne-sicherung
|
||||||
|
#
|
||||||
|
# Vor dem Aktualisieren wird gesichert – das dauert Sekunden und erspart schlechte Abende.
|
||||||
|
|
||||||
|
set -o errexit
|
||||||
|
set -o nounset
|
||||||
|
set -o pipefail
|
||||||
|
|
||||||
|
VERZEICHNIS=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||||
|
cd "$VERZEICHNIS"
|
||||||
|
|
||||||
|
blau() { printf '\033[1;36m› %s\033[0m\n' "$*"; }
|
||||||
|
gruen() { printf '\033[1;32m✓ %s\033[0m\n' "$*"; }
|
||||||
|
fehler() {
|
||||||
|
printf '\033[1;31m✗ %s\033[0m\n' "$*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
SICHERN="ja"
|
||||||
|
[ "${1:-}" = "--ohne-sicherung" ] && SICHERN="nein"
|
||||||
|
|
||||||
|
command -v docker >/dev/null 2>&1 || fehler "Docker nicht gefunden."
|
||||||
|
[ -f .env ] || fehler "Keine .env gefunden – wird dieses Skript im richtigen Verzeichnis ausgeführt?"
|
||||||
|
|
||||||
|
if [ "$SICHERN" = "ja" ]; then
|
||||||
|
blau "Sicherung anlegen …"
|
||||||
|
bash deploy/backup.sh
|
||||||
|
gruen "Sicherung abgeschlossen."
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -d .git ]; then
|
||||||
|
blau "Neuen Stand holen …"
|
||||||
|
git pull --ff-only
|
||||||
|
fi
|
||||||
|
|
||||||
|
blau "Abbilder neu bauen …"
|
||||||
|
docker compose build --pull
|
||||||
|
|
||||||
|
blau "Datenbank migrieren …"
|
||||||
|
docker compose run --rm migrate
|
||||||
|
|
||||||
|
blau "Dienste neu starten …"
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
# Kurz warten und nachsehen, ob das Dashboard wieder antwortet.
|
||||||
|
blau "Warte auf das Dashboard …"
|
||||||
|
for versuch in $(seq 1 30); do
|
||||||
|
if docker compose exec -T web wget -qO- http://localhost:3000/api/gesundheit >/dev/null 2>&1; then
|
||||||
|
gruen "ProxLedger läuft wieder (nach ${versuch} s)."
|
||||||
|
docker compose ps
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
fehler "Das Dashboard antwortet nicht. Protokoll ansehen: docker compose logs -n 50 web"
|
||||||
@@ -81,8 +81,16 @@ services:
|
|||||||
APP_URL: ${APP_URL:-http://localhost:3000}
|
APP_URL: ${APP_URL:-http://localhost:3000}
|
||||||
WORKER_CRON: ${WORKER_CRON:-0 * * * *}
|
WORKER_CRON: ${WORKER_CRON:-0 * * * *}
|
||||||
ZUSAMMENFASSUNG_CRON: ${ZUSAMMENFASSUNG_CRON:-0 7 * * *}
|
ZUSAMMENFASSUNG_CRON: ${ZUSAMMENFASSUNG_CRON:-0 7 * * *}
|
||||||
|
AUFRAEUM_CRON: ${AUFRAEUM_CRON:-30 3 * * *}
|
||||||
volumes:
|
volumes:
|
||||||
- uploads:/data
|
- uploads:/data
|
||||||
|
healthcheck:
|
||||||
|
# Der Worker spricht kein HTTP; geprüft wird schlicht, ob der Prozess noch da ist.
|
||||||
|
test: ["CMD-SHELL", "pgrep -f dist/worker.mjs > /dev/null"]
|
||||||
|
interval: 60s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
start_period: 20s
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
datenbank:
|
datenbank:
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# #026 Auslieferung finalisieren
|
||||||
|
**Status:** erledigt
|
||||||
|
**Meilenstein:** v0.17.0
|
||||||
|
**Labels:** deployment, doku
|
||||||
|
**Geschätzt:** 3 h **Tatsächlich:** 0 h 40 min
|
||||||
|
|
||||||
|
## Ziel
|
||||||
|
Zwei saubere Wege, ProxLedger in Betrieb zu nehmen: als Docker-Stack und als LXC-Container nach Art
|
||||||
|
der Proxmox VE Helper-Scripts.
|
||||||
|
|
||||||
|
## Aufgaben
|
||||||
|
- [x] `deploy/update.sh`: sichern, bauen, migrieren, neu starten, Erreichbarkeit prüfen
|
||||||
|
- [x] Zustandsprüfung für den Worker in `docker-compose.yml`
|
||||||
|
- [x] `AUFRAEUM_CRON` in `.env.example` und im Compose-Dienst
|
||||||
|
- [x] `deploy/community-scripts/ct/proxledger.sh` und `install/proxledger-install.sh`
|
||||||
|
- [x] `deploy/community-scripts/README.md` mit Voraussetzungen für eine Einreichung
|
||||||
|
- [x] README auf den tatsächlichen Stand gebracht (Feldtest erledigt, Einstellungen, Sicherheit)
|
||||||
|
- [x] `docs/roadmap.md`
|
||||||
|
- [x] Docker-Stapel vollständig durchgespielt: bauen, starten, migrieren, anmelden, Agent-Skript abrufen
|
||||||
|
|
||||||
|
## Erkenntnisse zu community-scripts
|
||||||
|
- Die Skripte laden ihr Gerüst inzwischen aus einem eigenen Repository
|
||||||
|
(`community-scripts/core`, Datei `core/build.func`) – nicht mehr aus `misc/build.func`.
|
||||||
|
- **Neue Skripte gehören als Pull Request nach ProxmoxVED**, nicht nach ProxmoxVE. Direkt eingereichte
|
||||||
|
Skripte werden dort ohne Prüfung geschlossen.
|
||||||
|
- Die JSON-Metadaten für die Webseite liegen nicht mehr im Skript-Repository.
|
||||||
|
- Eine Einreichung setzt öffentlich erreichbaren Quelltext voraus (in aller Regel GitHub). Solange
|
||||||
|
ProxLedger im eigenen Gitea liegt, bleiben die Skripte intern; die Quelle lässt sich über
|
||||||
|
`PROXLEDGER_REPO` setzen.
|
||||||
|
|
||||||
|
## Beim Prüfen gefunden
|
||||||
|
Der Docker-Stapel wurde vollständig gestartet (bauen, migrieren, anmelden). Dabei zeigte sich: Nach der
|
||||||
|
Anmeldung wies die Weiterleitung auf `http://<container-id>:3000` – die **interne** Adresse des
|
||||||
|
Containers. Im Browser hinter einem Reverse Proxy wäre das eine tote Adresse gewesen. Weiterleitungen
|
||||||
|
nehmen jetzt die eingetragene Adresse, sonst `APP_URL`, sonst die Kopfzeilen des Reverse Proxy
|
||||||
|
(`src/lib/adresse.ts`, vier Tests). Im Stapel gegengeprüft: `location: http://localhost:3001/`.
|
||||||
|
|
||||||
|
## Entscheidung
|
||||||
|
Der eigene `deploy/lxc-installer.sh` (Container **mit Docker darin**) bleibt erhalten. Der neue Weg
|
||||||
|
installiert Node und PostgreSQL direkt in den Container – das ist schlanker und entspricht dem, was
|
||||||
|
Proxmox-Betreiber von Helper-Scripts erwarten. Beide Wege stehen gleichberechtigt im README.
|
||||||
@@ -29,4 +29,5 @@ Details und Architektur: [../architektur.md](../architektur.md). Zeiten werden a
|
|||||||
| [#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) |
|
| [#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 |
|
| [#024](024-owasp-haertung.md) | Härtung nach OWASP | erledigt | v0.15.0 | 4 h | 0 h 45 min |
|
||||||
| [#025](025-einstellungen.md) | Einstellungen im Dashboard | erledigt | v0.16.0 | 5 h | 1 h 10 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** |
|
| [#026](026-auslieferung.md) | Auslieferung finalisieren | erledigt | v0.17.0 | 3 h | 0 h 40 min |
|
||||||
|
| | **Summe** | | | **133 h** | **12 h 18 min** |
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
# Roadmap
|
||||||
|
|
||||||
|
Stand: 19.09.2026, Fassung 0.17.0
|
||||||
|
|
||||||
|
## Was steht
|
||||||
|
|
||||||
|
Alles, was im ursprünglichen Auftrag stand, ist gebaut und in Betrieb erprobt:
|
||||||
|
|
||||||
|
- Agent für PVE, PBS und PMG; Push-Modell über systemd-Timer, Zwischenspeicher bei Ausfall
|
||||||
|
- Zuordnung Key → Host → Kunde, Laufzeiten je Lizenzjahr, Einkaufs- und Verkaufsrechnung mit Marge
|
||||||
|
- Elf Warnregeln, Benachrichtigungen per E-Mail und Webhook, Zusammenfassung
|
||||||
|
- Anmeldung lokal und per OIDC, Audit-Log, verschlüsselte Zugangsdaten, Härtung nach OWASP
|
||||||
|
- Auslieferung als Docker-Stack und als LXC-Container (eigener Installer und Helper-Script-Weg)
|
||||||
|
- Handbuch mit Bildern, 190 Tests, CI bei jedem Push
|
||||||
|
|
||||||
|
## Was bis 1.0.0 fehlt
|
||||||
|
|
||||||
|
Nichts davon ist eine Baustelle im Code – es fehlt **Erprobung** und eine Handvoll Kanten:
|
||||||
|
|
||||||
|
| Punkt | Warum es zählt | Aufwand |
|
||||||
|
|---|---|---|
|
||||||
|
| **Mail Gateway im Feldtest** (#023) | Der einzige Produktzweig, den nie ein echter Host bestätigt hat. Bei PBS lag die Annahme daneben, und der Agent hätte dort nichts gemeldet – bei PMG kann es genauso sein. | 1 h, sobald ein Host da ist |
|
||||||
|
| **PBS mit aktiver Subscription** (#023) | Bisher nur der Fall „keine Subscription“ geprüft. Ablaufdatum, Sockel und Level sind auf PBS ungeprüft. | 30 min |
|
||||||
|
| **Ein Monat Dauerbetrieb** | Timer, Nachsendung, nächtliches Aufräumen und die Zusammenfassung laufen erst seit einem Tag. Ein Lizenzablauf in echt wäre der beste Beweis. | Zeit, keine Arbeit |
|
||||||
|
| **Sicherung einmal zurückspielen** | `deploy/backup.sh` schreibt Sicherungen. Eine Sicherung, die nie zurückgespielt wurde, ist keine Sicherung. | 30 min |
|
||||||
|
|
||||||
|
Danach ist 1.0.0 gerechtfertigt. Meine Empfehlung: erst laufen lassen, dann das Etikett vergeben.
|
||||||
|
|
||||||
|
## Nach 1.0 – Vorschläge, nach Nutzen sortiert
|
||||||
|
|
||||||
|
### Lohnt sich am ehesten
|
||||||
|
|
||||||
|
1. **Verlängerung mit einem Klick.** Heute wird eine Laufzeit bestätigt und die Rechnung von Hand
|
||||||
|
erfasst. Ein Knopf „verlängern“, der die Folgelaufzeit anlegt, den Einkauf vormerkt und die
|
||||||
|
Kundenrechnung mit dem eingestellten Aufschlag vorbereitet, spart bei jedem Lizenzjahr Klicks.
|
||||||
|
2. **Rechnungsnummern automatisch.** Ein Schema wie `PL-{jahr}-{nummer}` mit fortlaufendem Zähler;
|
||||||
|
heute tippt man jede Nummer. (In #025 bewusst weggelassen, weil es eine Vergabelogik braucht.)
|
||||||
|
3. **Mehrere Benutzer mit Rollen.** Derzeit darf jeder Angemeldete alles – in `docs/sicherheit.md`
|
||||||
|
offen vermerkt. Für Buchhaltung (nur Rechnungen) und Technik (nur Hosts) wären zwei Rollen genug.
|
||||||
|
Dazu gehört eine Benutzerverwaltung in der Oberfläche statt auf der Kommandozeille.
|
||||||
|
4. **Währung und Zeitzone auch in der Browseransicht.** Beides steht schon in den Einstellungen, wirkt
|
||||||
|
aber nur in PDF, CSV und Benachrichtigungen; die Oberfläche rechnet fest in Euro.
|
||||||
|
|
||||||
|
### Sinnvoll, wenn es wächst
|
||||||
|
|
||||||
|
5. **Kundenportal oder Bericht per E-Mail.** Ein monatlicher Stand je Kunde, automatisch verschickt –
|
||||||
|
spart Rückfragen.
|
||||||
|
6. **Angebot vor der Verlängerung.** Aus der Jahresbahn heraus ein PDF-Angebot für das kommende
|
||||||
|
Lizenzjahr erzeugen.
|
||||||
|
7. **Import der Proxmox-Shop-Rechnungen.** Wer dort per Sammelrechnung kauft, tippt heute jede Position
|
||||||
|
ab. Ein CSV- oder PDF-Import mit Zuordnung über den Key wäre eine echte Ersparnis.
|
||||||
|
8. **Metriken für Prometheus** (`/metrics`): Hosts je Zustand, Tage bis zum nächsten Ablauf, Alter des
|
||||||
|
letzten Reports. Wer schon Grafana betreibt, sieht ProxLedger damit im gewohnten Bild.
|
||||||
|
9. **Zwei-Faktor-Anmeldung** für den lokalen Zugang. Heute lautet die Antwort „dann nimm OIDC“ – das
|
||||||
|
ist vertretbar, aber nicht für jeden.
|
||||||
|
|
||||||
|
### Nur falls der Bedarf entsteht
|
||||||
|
|
||||||
|
10. **Mandantenfähigkeit.** Mehrere Systemhäuser in einer Installation. Das ist ein Umbau, kein
|
||||||
|
Anbau – die Zugriffskontrolle müsste durchgängig auf Mandanten umgestellt werden.
|
||||||
|
11. **Agent für weitere Produkte** (Proxmox Datacenter Manager), sobald der Bedarf da ist.
|
||||||
|
12. **Hochverfügbarkeit.** Die Ratenbremse liegt im Prozessspeicher; für mehrere Instanzen müsste sie
|
||||||
|
in die Datenbank wandern. Bei einem Dashboard für ein Systemhaus ist das Aufwand ohne Gewinn.
|
||||||
|
|
||||||
|
## Was ich bewusst nicht vorschlage
|
||||||
|
|
||||||
|
- **Pull statt Push.** Das Dashboard würde dann Zugänge zu allen Kundenhosts brauchen. Der jetzige Weg
|
||||||
|
kommt ohne gespeicherte root-Zugänge aus; das ist ein Sicherheitsgewinn, den man nicht aufgeben sollte.
|
||||||
|
- **Eigene Benutzer- und Rechteverwaltung mit Gruppen, Vererbung und allem.** Wer das braucht, hat schon
|
||||||
|
ein IdP – dafür gibt es OIDC.
|
||||||
|
- **Agent mit Abhängigkeiten** (Python, jq). Er kommt heute mit Bash und curl aus und läuft damit auf
|
||||||
|
jedem Proxmox-Host, auch auf einem alten.
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "proxledger",
|
"name": "proxledger",
|
||||||
"version": "0.16.0",
|
"version": "0.17.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { cookies, headers } from "next/headers";
|
import { cookies, headers } from "next/headers";
|
||||||
|
import { eigeneAdresse } from "@/lib/adresse";
|
||||||
import { clientIp } from "@/lib/ratelimit";
|
import { clientIp } from "@/lib/ratelimit";
|
||||||
import { cookieOptionen, SITZUNGS_COOKIE, sitzungErzeugen } from "@/lib/sitzung";
|
import { cookieOptionen, SITZUNGS_COOKIE, sitzungErzeugen } from "@/lib/sitzung";
|
||||||
import { AnmeldeFehler, mitPasswortAnmelden } from "@/server/auth/benutzer";
|
import { AnmeldeFehler, mitPasswortAnmelden } from "@/server/auth/benutzer";
|
||||||
@@ -38,6 +39,7 @@ export function herkunftStimmt(request: Request): boolean {
|
|||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
const formular = await request.formData();
|
const formular = await request.formData();
|
||||||
|
const { dashboardUrl } = await einstellungen();
|
||||||
const email = formular.get("email")?.toString().trim() ?? "";
|
const email = formular.get("email")?.toString().trim() ?? "";
|
||||||
const passwort = formular.get("passwort")?.toString() ?? "";
|
const passwort = formular.get("passwort")?.toString() ?? "";
|
||||||
const zielEingabe = formular.get("ziel")?.toString() || "/";
|
const zielEingabe = formular.get("ziel")?.toString() || "/";
|
||||||
@@ -45,7 +47,7 @@ export async function POST(request: Request) {
|
|||||||
const ziel = zielEingabe.startsWith("/") && !zielEingabe.startsWith("//") ? zielEingabe : "/";
|
const ziel = zielEingabe.startsWith("/") && !zielEingabe.startsWith("//") ? zielEingabe : "/";
|
||||||
|
|
||||||
function zurueckZurAnmeldung(meldung: string) {
|
function zurueckZurAnmeldung(meldung: string) {
|
||||||
const adresse = new URL("/login", request.url);
|
const adresse = eigeneAdresse(request, "/login", dashboardUrl);
|
||||||
adresse.searchParams.set("fehler", meldung);
|
adresse.searchParams.set("fehler", meldung);
|
||||||
if (ziel !== "/") adresse.searchParams.set("ziel", ziel);
|
if (ziel !== "/") adresse.searchParams.set("ziel", ziel);
|
||||||
return Response.redirect(adresse, 303);
|
return Response.redirect(adresse, 303);
|
||||||
@@ -75,7 +77,7 @@ export async function POST(request: Request) {
|
|||||||
);
|
);
|
||||||
(await cookies()).set(SITZUNGS_COOKIE, token, cookieOptionen(sitzungsdauerStunden));
|
(await cookies()).set(SITZUNGS_COOKIE, token, cookieOptionen(sitzungsdauerStunden));
|
||||||
|
|
||||||
return Response.redirect(new URL(ziel, request.url), 303);
|
return Response.redirect(eigeneAdresse(request, ziel, dashboardUrl), 303);
|
||||||
} catch (fehler) {
|
} catch (fehler) {
|
||||||
if (fehler instanceof AnmeldeFehler) return zurueckZurAnmeldung(fehler.message);
|
if (fehler instanceof AnmeldeFehler) return zurueckZurAnmeldung(fehler.message);
|
||||||
console.error("Anmeldung fehlgeschlagen:", fehler);
|
console.error("Anmeldung fehlgeschlagen:", fehler);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { cookies } from "next/headers";
|
import { cookies } from "next/headers";
|
||||||
|
import { eigeneAdresse } from "@/lib/adresse";
|
||||||
import { cookieOptionen, SITZUNGS_COOKIE, sitzungErzeugen } from "@/lib/sitzung";
|
import { cookieOptionen, SITZUNGS_COOKIE, sitzungErzeugen } from "@/lib/sitzung";
|
||||||
import { mitOidcAnmelden } from "@/server/auth/benutzer";
|
import { mitOidcAnmelden } from "@/server/auth/benutzer";
|
||||||
import { anmeldungAbschliessen, oidcEinstellungen } from "@/server/auth/oidc";
|
import { anmeldungAbschliessen, oidcEinstellungen } from "@/server/auth/oidc";
|
||||||
@@ -18,8 +19,10 @@ export async function GET(request: Request) {
|
|||||||
speicher.delete(name);
|
speicher.delete(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { dashboardUrl } = await systemEinstellungen();
|
||||||
|
|
||||||
if (!einstellungen || !pruefwert || !zustand) {
|
if (!einstellungen || !pruefwert || !zustand) {
|
||||||
return Response.redirect(new URL("/login?fehler=oidc", request.url), 302);
|
return Response.redirect(eigeneAdresse(request, "/login?fehler=oidc", dashboardUrl), 302);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -38,9 +41,9 @@ export async function GET(request: Request) {
|
|||||||
);
|
);
|
||||||
speicher.set(SITZUNGS_COOKIE, token, cookieOptionen(sitzungsdauerStunden));
|
speicher.set(SITZUNGS_COOKIE, token, cookieOptionen(sitzungsdauerStunden));
|
||||||
|
|
||||||
return Response.redirect(new URL(ziel.startsWith("/") ? ziel : "/", request.url), 302);
|
return Response.redirect(eigeneAdresse(request, ziel.startsWith("/") ? ziel : "/", dashboardUrl), 302);
|
||||||
} catch (fehler) {
|
} catch (fehler) {
|
||||||
console.error("OIDC-Rückkehr fehlgeschlagen:", fehler);
|
console.error("OIDC-Rückkehr fehlgeschlagen:", fehler);
|
||||||
return Response.redirect(new URL("/login?fehler=oidc", request.url), 302);
|
return Response.redirect(eigeneAdresse(request, "/login?fehler=oidc", dashboardUrl), 302);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
/**
|
||||||
|
* Ermittelt die Adresse, unter der das Dashboard **von außen** erreichbar ist.
|
||||||
|
*
|
||||||
|
* Im Container ist `request.url` die interne Adresse (`http://<container-id>:3000`). Eine Weiterleitung
|
||||||
|
* dorthin führt im Browser ins Leere – genau das passierte nach der Anmeldung im Docker-Stapel.
|
||||||
|
*
|
||||||
|
* Reihenfolge: eingetragene Adresse → `APP_URL` → Kopfzeilen des Reverse Proxy → Anfrage selbst.
|
||||||
|
* Die Kopfzeilen stehen bewusst hinter den konfigurierten Werten: Sie kommen vom Client und lassen
|
||||||
|
* sich fälschen; für eine Weiterleitung auf die eigene Seite ist das unkritisch, als erste Wahl aber
|
||||||
|
* unnötig.
|
||||||
|
*/
|
||||||
|
export function oeffentlicheBasis(request: Request, eingetragen?: string): URL {
|
||||||
|
if (eingetragen) {
|
||||||
|
try {
|
||||||
|
return new URL(eingetragen);
|
||||||
|
} catch {
|
||||||
|
// Unbrauchbarer Eintrag: weiter mit den übrigen Quellen.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.APP_URL) {
|
||||||
|
try {
|
||||||
|
return new URL(process.env.APP_URL);
|
||||||
|
} catch {
|
||||||
|
// ebenso
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const gastgeber = request.headers.get("x-forwarded-host") ?? request.headers.get("host");
|
||||||
|
if (gastgeber) {
|
||||||
|
const schema = request.headers.get("x-forwarded-proto") ?? new URL(request.url).protocol.replace(":", "");
|
||||||
|
try {
|
||||||
|
return new URL(`${schema}://${gastgeber}`);
|
||||||
|
} catch {
|
||||||
|
// ebenso
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new URL(request.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Baut eine Adresse auf derselben Installation – für Weiterleitungen nach der Anmeldung. */
|
||||||
|
export function eigeneAdresse(request: Request, pfad: string, eingetragen?: string): URL {
|
||||||
|
return new URL(pfad, oeffentlicheBasis(request, eingetragen));
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import { herkunftStimmt } from "@/app/api/auth/anmelden/route";
|
import { herkunftStimmt } from "@/app/api/auth/anmelden/route";
|
||||||
|
import { eigeneAdresse } from "@/lib/adresse";
|
||||||
import type { GeleseneSitzung } from "@/lib/sitzung";
|
import type { GeleseneSitzung } from "@/lib/sitzung";
|
||||||
import { sitzungBewerten } from "@/server/auth/sitzungspflicht";
|
import { sitzungBewerten } from "@/server/auth/sitzungspflicht";
|
||||||
|
|
||||||
@@ -72,3 +73,35 @@ describe("Herkunft der Anmeldung", () => {
|
|||||||
).toBe(true);
|
).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Öffentliche Adresse", () => {
|
||||||
|
// APP_URL kommt sonst aus der .env der Entwicklungsumgebung und verfälscht die Rückfallstufen.
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.stubEnv("APP_URL", "");
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
vi.unstubAllEnvs();
|
||||||
|
});
|
||||||
|
|
||||||
|
const anfrageMit = (kopfzeilen: Record<string, string>) =>
|
||||||
|
new Request("http://0a5963a591df:3000/api/auth/anmelden", { method: "POST", headers: kopfzeilen });
|
||||||
|
|
||||||
|
it("nimmt die eingetragene Adresse, nicht die interne des Containers", () => {
|
||||||
|
const adresse = eigeneAdresse(anfrageMit({}), "/hosts", "https://proxledger.example.de");
|
||||||
|
expect(adresse.toString()).toBe("https://proxledger.example.de/hosts");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("fällt auf die Kopfzeilen des Reverse Proxy zurück", () => {
|
||||||
|
const anfrage = anfrageMit({ "x-forwarded-host": "dashboard.kunde.de", "x-forwarded-proto": "https" });
|
||||||
|
expect(eigeneAdresse(anfrage, "/").toString()).toBe("https://dashboard.kunde.de/");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("nimmt notfalls die Anfrage selbst", () => {
|
||||||
|
expect(eigeneAdresse(anfrageMit({}), "/").host).toBe("0a5963a591df:3000");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("übergeht einen unbrauchbaren Eintrag, statt zu scheitern", () => {
|
||||||
|
const anfrage = anfrageMit({ "x-forwarded-host": "dashboard.kunde.de" });
|
||||||
|
expect(eigeneAdresse(anfrage, "/", "keine-adresse").host).toBe("dashboard.kunde.de");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user