Files
Archivum/backend/internal/config/config.go
Bjorn Blomberg 5819a6bdce
All checks were successful
build-and-push / build (push) Successful in 1m7s
feat(gitsync): webhook-triggad synk + helt manuellt läge
- POST /api/git-hook: HMAC-SHA256-verifierad (X-Gitea-Signature),
  reagerar bara på pushar till live-grenen, svarar 202 och synkar async
- webhook-hemlighet genereras server-side (headless via config.json),
  roteras med gitRegenerateWebhookSecret
- auto_sync_minutes=0 + webhook av ⇒ ingen automatisk hämtning alls;
  webhook på ⇒ catch-up-synk vid uppstart (missade event)
- admin-UI: webhook-toggle, target-URL + secret med copy/rotate

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 23:52:01 +02:00

157 lines
4.9 KiB
Go

package config
import (
"encoding/json"
"errors"
"os"
"strings"
)
// Config holds all backend runtime configuration.
type Config struct {
StoragePath string `json:"storage_path"`
DBPath string `json:"db_path"`
LDAP LDAPConfig `json:"ldap"`
OIDC OIDCConfig `json:"oidc"`
JWTSecret string `json:"jwt_secret"`
ListenAddr string `json:"listen_addr"`
// PublicURL is the externally reachable base URL of Archivum
// (e.g. https://archivum.brasse-pc.eu). Used to build the OIDC
// redirect URL when one is not set explicitly.
PublicURL string `json:"public_url"`
GitSync GitSyncConfig `json:"git_sync"`
}
// GitSyncConfig configures synchronisation of the wiki git repository
// with a remote (SSH or HTTPS). The SSH keypair is generated by the
// server itself and lives next to config.json so the whole feature can
// be driven headlessly by editing config.json and restarting.
type GitSyncConfig struct {
Enabled bool `json:"enabled"`
RemoteURL string `json:"remote_url"` // e.g. ssh://git@git.brasse-pc.eu:2222/brasse/Archivum-documets.git
// LiveBranch is the branch that counts as the live content of this
// instance: auto-sync pulls/pushes it and resets target it.
LiveBranch string `json:"live_branch"`
// ReadOnly forbids every push to the remote; the instance only
// pulls. Local edits still commit locally.
ReadOnly bool `json:"read_only"`
// AutoSyncMinutes > 0 enables background sync on that interval.
// Background pulls are fast-forward only; anything needing a real
// merge is left for an admin to resolve in the UI. With 0 and
// webhook disabled, nothing is fetched automatically at all.
AutoSyncMinutes int `json:"auto_sync_minutes"`
// WebhookEnabled exposes POST /api/git-hook so the git host can
// trigger a sync on push instead of (or besides) polling.
WebhookEnabled bool `json:"webhook_enabled"`
// WebhookSecret authenticates hook calls (HMAC-SHA256 signature in
// X-Gitea-Signature). Generated by the server when left empty.
WebhookSecret string `json:"webhook_secret"`
}
// Normalize fills in defaults for optional git-sync fields.
func (g *GitSyncConfig) Normalize() {
if g.LiveBranch == "" {
g.LiveBranch = "main"
}
if g.AutoSyncMinutes < 0 {
g.AutoSyncMinutes = 0
}
}
type LDAPConfig struct {
Url string `json:"url"`
BaseDN string `json:"base_dn"`
AdminUser string `json:"admin_user"`
AdminPass string `json:"admin_pass"`
}
// OIDCConfig configures Single-Sign-On via an OpenID Connect provider
// (Authentik in this deployment). The provider authenticates the user and
// returns their group membership in the groups claim.
type OIDCConfig struct {
Enabled bool `json:"enabled"`
Issuer string `json:"issuer"` // e.g. https://authentik.brasse-pc.eu/application/o/archivum/
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret"`
RedirectURL string `json:"redirect_url"` // optional; derived from PublicURL when empty
// Claim/group mapping. Sensible defaults are applied in Normalize().
GroupsClaim string `json:"groups_claim"` // default "groups"
UsernameClaim string `json:"username_claim"` // default "preferred_username"
AdminGroup string `json:"admin_group"` // default "Archivum-admin"
ReaderGroup string `json:"reader_group"` // default "Archivum-reader"
}
// Normalize fills in defaults for optional OIDC fields.
func (o *OIDCConfig) Normalize() {
if o.GroupsClaim == "" {
o.GroupsClaim = "groups"
}
if o.UsernameClaim == "" {
o.UsernameClaim = "preferred_username"
}
if o.AdminGroup == "" {
o.AdminGroup = "Archivum-admin"
}
if o.ReaderGroup == "" {
o.ReaderGroup = "Archivum-reader"
}
}
// ResolvedRedirectURL returns the OIDC callback URL, deriving it from
// PublicURL when RedirectURL is not set.
func (c *Config) ResolvedRedirectURL() string {
if c.OIDC.RedirectURL != "" {
return c.OIDC.RedirectURL
}
if c.PublicURL != "" {
return strings.TrimRight(c.PublicURL, "/") + "/auth/oidc/callback"
}
return ""
}
// ErrRequireSetup is returned when config is missing or empty,
// signalling that the frontend should start the Setup Wizard.
var ErrRequireSetup = errors.New("REQUIRE_SETUP")
func Load(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil, ErrRequireSetup
}
return nil, err
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, err
}
if cfg.StoragePath == "" || cfg.JWTSecret == "" {
return nil, ErrRequireSetup
}
if cfg.ListenAddr == "" {
cfg.ListenAddr = ":4000"
}
// Default DB path to the dedicated db volume so it can be backed up
// independently of the wiki content.
if cfg.DBPath == "" {
cfg.DBPath = "/data/db/archivum.db"
}
cfg.OIDC.Normalize()
cfg.GitSync.Normalize()
return &cfg, nil
}
func Save(path string, cfg *Config) error {
data, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
return err
}
return os.WriteFile(path, data, 0600)
}