package config import ( "fmt" "os" "github.com/BurntSushi/toml" "github.com/rs/zerolog/log" "gitlab.com/mstarongitlab/goutils/other" ) type ConfigSSL struct { HandleSSL bool `toml:"handle_ssl"` // Whether Linstrom should handle SSL encryption itself // If Linstrom is to handle SSL, whether it should use LetsEncrypt for certificates UseLetsEncrypt *bool `toml:"use_lets_encrypt"` // Path to the certificate if Linstrom is to handle SSL while not using LetsEncrypt CertificateFile *string `toml:"certificate_file"` // Mail adress to use in case of using LetsEncrypt AdminMail *string `toml:"admin_mail"` } type ConfigGeneral struct { Protocol string `toml:"protocol"` // The protocol with which to access the server publicly (http/https) // The subdomain under which the server lives (example: "linstrom" if the full domain is linstrom.example.com) Subdomain *string `toml:"subdomain"` // The root domain under which the server lives (example: "example.com" if the full domain is linstrom.example.com) Domain string `toml:"domain"` // The port on which the server runs on PrivatePort int `toml:"private_port"` // The port under which the public can reach the server (useful if running behind a reverse proxy) PublicPort *int `toml:"public_port"` // File to write structured logs to (structured being formatted as json) // If not set, Linstrom won't write structured logs StructuredLogFile *string } type ConfigWebAuthn struct { DisplayName string `toml:"display_name"` HashingSecret string `toml:"hashing_secret"` } type ConfigAdmin struct { // Name of the server's root admin account Username string `toml:"username"` // A one time password used to verify account access to the root admin // after a server has been created and before the account could be linked to a passkey FirstTimeSetupOTP string `toml:"first_time_setup_otp"` } type ConfigStorage struct { Host string `toml:"host"` Username string `toml:"username"` Password string `toml:"password"` DbName string `toml:"db_name"` Port int `toml:"port"` SslMode *string `toml:"ssl_mode"` TimeZone *string `toml:"time_zone"` // Url to redis server. If empty, no redis is used RedisUrl *string `toml:"redis_url,omitempty"` // The maximum size of the in-memory cache in bytes MaxInMemoryCacheSize int64 `toml:"max_in_memory_cache_size"` // The time to live for in app in memory cache items, in seconds MaxInMemoryCacheTTL int // The time to live for items in redis, in seconds MaxRedisCacheTTL *int } type ConfigMail struct { Host string `toml:"host"` Port int `toml:"port"` Username string `toml:"username"` Password string `toml:"password"` EncryptionOverwrite *string `toml:"encryption_overwrite,omitempty"` KeepAliveOverwrite *bool `toml:"keep_alive_overwrite,omitempty"` ConnectTimeoutSecondsOverwrite *int `toml:"connect_timeout_seconds_overwrite,omitempty"` SendTimeoutSecondsOverwrite *int `toml:"send_timeout_seconds_overwrite,omitempty"` TemplateOverwriteDirectory *string `toml:"template_overwrite_directory,omitempty"` } type Config struct { General ConfigGeneral `toml:"general"` SSL ConfigSSL `toml:"ssl"` Admin ConfigAdmin `toml:"admin"` Webauthn ConfigWebAuthn `toml:"webauthn"` Storage ConfigStorage `toml:"storage"` Mail ConfigMail `toml:"mail"` } var GlobalConfig Config var defaultConfig Config = Config{ General: ConfigGeneral{ Protocol: "http", Subdomain: nil, Domain: "localhost", PrivatePort: 8080, PublicPort: nil, }, SSL: ConfigSSL{ HandleSSL: false, UseLetsEncrypt: nil, CertificateFile: nil, AdminMail: nil, }, Admin: ConfigAdmin{ Username: "server-admin", FirstTimeSetupOTP: "Example otp password", }, Webauthn: ConfigWebAuthn{ DisplayName: "Linstrom", HashingSecret: "some super secure secret that should never be changed or else password storage breaks", }, Storage: ConfigStorage{ Host: "localhost", Username: "linstrom", Password: "linstrom", DbName: "linstrom", Port: 5432, SslMode: other.IntoPointer("disable"), TimeZone: other.IntoPointer("Europe/Berlin"), RedisUrl: nil, MaxInMemoryCacheSize: 1e6, // 1 Megabyte MaxInMemoryCacheTTL: 5, MaxRedisCacheTTL: nil, }, } func (gc *ConfigGeneral) GetFullDomain() string { if gc.Subdomain != nil { return *gc.Subdomain + gc.Domain } return gc.Domain } func (gc *ConfigGeneral) GetFullPublicUrl() string { str := gc.Protocol + gc.GetFullDomain() if gc.PublicPort != nil { str += fmt.Sprint(*gc.PublicPort) } else { str += fmt.Sprint(gc.PrivatePort) } return str } func (sc *ConfigStorage) BuildPostgresDSN() string { dsn := fmt.Sprintf( "host=%s user=%s password=%s dbname=%s port=%d", sc.Host, sc.Username, sc.Password, sc.DbName, sc.Port, ) if sc.SslMode != nil { dsn += fmt.Sprintf(" sslmode=%s", *sc.SslMode) } if sc.TimeZone != nil { dsn += fmt.Sprintf(" TimeZone=%s", *sc.TimeZone) } return dsn } func WriteDefaultConfig(toFile string) error { log.Trace().Caller().Send() log.Info().Str("config-file", toFile).Msg("Writing default config to file") file, err := os.Create(toFile) if err != nil { log.Error(). Err(err). Str("config-file", toFile). Msg("Failed to create file for default config") return err } defer file.Close() data, err := toml.Marshal(&defaultConfig) if err != nil { log.Error().Err(err).Msg("Failed to marshal default config to toml") return err } _, err = file.Write(data) if err != nil { log.Error().Err(err).Str("config-file", toFile).Msg("Failed to write default config") return err } log.Info().Str("config-file", toFile).Msg("Wrote default config") return nil } func ReadAndWriteToGlobal(fileName string) error { log.Trace().Caller().Send() log.Debug().Str("config-file", fileName).Msg("Attempting to read config file") data, err := os.ReadFile(fileName) if err != nil { log.Warn(). Str("config-file", fileName). Err(err). Msg("Failed to read config file, attempting to write default config") err = WriteDefaultConfig(fileName) if err != nil { log.Error(). Err(err). Str("config-file", fileName). Msg("Failed to create default config file") return err } GlobalConfig = defaultConfig return nil } config := Config{} log.Debug().Str("config-file", fileName).Msg("Read config file, attempting to unmarshal") err = toml.Unmarshal(data, &config) if err != nil { log.Error().Err(err).Bytes("config-data-raw", data).Msg("Failed to unmarshal config file") return err } GlobalConfig = config log.Info().Str("config-file", fileName).Msg("Read and applied config file") return nil }