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