config.go
⎇
Raw
1// Package config liest die gesamte Konfiguration aus Umgebungsvariablen.
2package config
3
4import (
5 "errors"
6 "fmt"
7 "log/slog"
8 "os"
9 "path/filepath"
10 "strconv"
11 "strings"
12)
13
14type Config struct {
15 Port int
16 DataDir string
17 DBPath string
18 MediaDir string
19 DocsDir string
20 BackupDir string
21 SMTP SMTP
22 PublicURL string // von außen erreichbare Adresse für Kalender-Links (z. B. https://physio.example.com)
23 Address string // Praxisadresse, Standard-Ort für Termine
24 BackupKeep int
25 Username string
26 PasswordHash string
27 PracticeName string
28 BaseURL string
29 Seed bool
30 LogLevel slog.Level
31 LogJSON bool
32}
33
34// SMTP für den Versand von Zusammenfassungen. Leerer Host = Versand aus.
35type SMTP struct {
36 Host, User, Password, From string
37 Port int
38 Security string // "ssl" (implizites TLS, 465), "starttls" (587) oder "none" (nur Tests/LAN)
39}
40
41func (s SMTP) Enabled() bool { return s.Host != "" && s.From != "" }
42
43func (c *Config) IsHTTPS() bool { return strings.HasPrefix(c.BaseURL, "https://") }
44
45func get(k, def string) string {
46 if v, ok := os.LookupEnv("PHYSIOBASE_" + k); ok && v != "" {
47 return v
48 }
49 return def
50}
51
52// Load liest die Konfiguration. Ohne Benutzername und Passwort-Hash startet die
53// App nicht – es gibt keinen Modus ohne Anmeldung.
54func Load() (*Config, error) {
55 c := &Config{
56 DataDir: get("DATA_DIR", "./data"),
57 Username: get("USERNAME", ""),
58 PasswordHash: get("PASSWORD_HASH", ""),
59 PracticeName: get("PRACTICE_NAME", "PhysioBase"),
60 BaseURL: strings.TrimRight(get("BASE_URL", ""), "/"),
61 }
62 c.PublicURL = strings.TrimRight(get("PUBLIC_URL", c.BaseURL), "/")
63 c.Address = get("PRACTICE_ADDRESS", "")
64 c.DBPath = get("DB_PATH", filepath.Join(c.DataDir, "physiobase.db"))
65 c.MediaDir = get("MEDIA_DIR", filepath.Join(c.DataDir, "medien"))
66 c.DocsDir = get("DOCS_DIR", filepath.Join(c.DataDir, "patienten"))
67 c.BackupDir = get("BACKUP_DIR", filepath.Join(c.DataDir, "backup"))
68
69 var err error
70 if c.Port, err = strconv.Atoi(get("PORT", "8080")); err != nil || c.Port < 1 || c.Port > 65535 {
71 return nil, fmt.Errorf("PHYSIOBASE_PORT ungültig: %q", get("PORT", ""))
72 }
73 if c.BackupKeep, err = strconv.Atoi(get("BACKUP_KEEP", "14")); err != nil {
74 return nil, fmt.Errorf("PHYSIOBASE_BACKUP_KEEP ungültig: %w", err)
75 }
76 c.SMTP = SMTP{
77 Host: get("SMTP_HOST", ""), User: get("SMTP_USER", ""), Password: get("SMTP_PASSWORD", ""),
78 From: get("SMTP_FROM", get("SMTP_USER", "")), Security: get("SMTP_SECURITY", "starttls"),
79 }
80 if c.SMTP.Port, err = strconv.Atoi(get("SMTP_PORT", "587")); err != nil {
81 return nil, fmt.Errorf("PHYSIOBASE_SMTP_PORT ungültig: %w", err)
82 }
83 switch c.SMTP.Security {
84 case "ssl", "starttls", "none":
85 default:
86 return nil, fmt.Errorf("PHYSIOBASE_SMTP_SECURITY muss ssl, starttls oder none sein, nicht %q", c.SMTP.Security)
87 }
88 if c.Seed, err = strconv.ParseBool(get("SEED", "1")); err != nil {
89 return nil, fmt.Errorf("PHYSIOBASE_SEED ungültig: %w", err)
90 }
91 if err := c.LogLevel.UnmarshalText([]byte(get("LOG_LEVEL", "info"))); err != nil {
92 return nil, fmt.Errorf("PHYSIOBASE_LOG_LEVEL ungültig: %w", err)
93 }
94 switch f := get("LOG_FORMAT", "text"); f {
95 case "text":
96 case "json":
97 c.LogJSON = true
98 default:
99 return nil, fmt.Errorf("PHYSIOBASE_LOG_FORMAT muss text oder json sein, nicht %q", f)
100 }
101
102 if c.Username == "" || c.PasswordHash == "" {
103 return nil, errors.New("PHYSIOBASE_USERNAME und PHYSIOBASE_PASSWORD_HASH sind Pflicht (Hash erzeugen: physiobase hash-password)")
104 }
105 if !strings.HasPrefix(c.PasswordHash, "$2") {
106 return nil, errors.New("PHYSIOBASE_PASSWORD_HASH ist kein bcrypt-Hash")
107 }
108 return c, nil
109}
110