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) }