feat(check,serve): Startvalidierung, HTTPS-Server, Signal-Handling und Versionscheck

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NBHF4R9EAejDJUMdwr6C68
This commit is contained in:
Carsten Abele 2026-08-14 09:34:22 +02:00
parent 7e23df9fec
commit 2d223c5823
9 changed files with 1158 additions and 6 deletions

View file

@ -0,0 +1,94 @@
//go:build uidemo
// Dieser Test startet das Portal mit Attrappen für AD und Firewall auf
// http://127.0.0.1:8080, damit die Oberfläche ohne echte Backends begutachtet
// werden kann. Aufruf:
//
// go test -tags uidemo ./cmd/vpnportal/ -run TestUIDemo -v -timeout 0
//
// Anmeldung mit beliebigem Benutzernamen und Passwort.
// Browser erlauben Secure-Cookies auf 127.0.0.1, deshalb funktioniert der
// gesamte Ablauf auch ohne TLS.
package main
import (
"context"
"io"
"net/http"
"os"
"strings"
"testing"
"time"
"git.ravensburg.dev/cabele/opnsense-portal/internal/audit"
"git.ravensburg.dev/cabele/opnsense-portal/internal/auth"
"git.ravensburg.dev/cabele/opnsense-portal/internal/web"
)
type demoAuth struct{}
func (demoAuth) Authenticate(ctx context.Context, u, p string) (*auth.Identity, error) {
return &auth.Identity{Username: "mmueller", Groups: []string{"VPN-Users"}}, nil
}
type demoCerts struct{}
func (demoCerts) EntriesFor(ctx context.Context, user string) ([]web.CertEntry, error) {
return []web.CertEntry{
{Token: "1:a1", InstanceName: "VPN Homeoffice", CommonName: "mmueller",
ValidTo: time.Now().AddDate(1, 2, 0)},
{Token: "2:b1", InstanceName: "VPN Außendienst", CommonName: "mmueller",
ValidTo: time.Now().AddDate(0, 0, 12)},
}, nil
}
func (demoCerts) Export(ctx context.Context, user, vpnID, refID, format string) (*web.ExportStream, error) {
return &web.ExportStream{
Filename: "vpn-demo-mmueller.ovpn",
ContentType: "application/x-openvpn-profile",
Body: io.NopCloser(strings.NewReader("client\nremote fw01.testfirma.local 1194\n")),
InstanceName: "VPN Homeoffice",
CommonName: "mmueller",
ValidTo: time.Now().AddDate(1, 0, 0),
}, nil
}
func (demoCerts) Pattern(user string) string { return user }
// leereCerts zeigt den Zustand "kein Zertifikat hinterlegt".
type leereCerts struct{ demoCerts }
func (leereCerts) EntriesFor(ctx context.Context, user string) ([]web.CertEntry, error) {
return nil, nil
}
type demoAudit struct{ t *testing.T }
func (d demoAudit) Log(e audit.Event) { d.t.Logf("audit: %+v", e) }
func TestUIDemo(t *testing.T) {
rn, err := web.NewRenderer(web.Brand{Title: "VPN-Portal Testfirma", Version: "uidemo"})
if err != nil {
t.Fatal(err)
}
// Mit VPNPORTAL_UIDEMO_EMPTY=1 den Zustand "kein Zertifikat" begutachten.
var certs web.CertSource = demoCerts{}
if os.Getenv("VPNPORTAL_UIDEMO_EMPTY") != "" {
certs = leereCerts{}
}
srv, err := web.NewServer(web.Deps{
Auth: demoAuth{}, Certs: certs, Audit: demoAudit{t},
Sessions: web.NewSessionStore(30*time.Minute, nil),
Limiter: web.NewLimiter(nil), Renderer: rn,
SupportContact: "it-helpdesk@testfirma.de",
SessionTTL: 30 * time.Minute,
MinLoginDuration: time.Millisecond,
})
if err != nil {
t.Fatal(err)
}
t.Log("Oberfläche unter http://127.0.0.1:8080 — mit Strg-C beenden")
if err := http.ListenAndServe("127.0.0.1:8080", srv.Handler()); err != nil {
t.Fatal(err)
}
}