package config import ( "flag" "net/url" "os" "slices" "strconv" // "strings" ) const ( dsnOsStr = "LX_DSN" dsnDefault = "lx.db" adsnOsStr = "LX_ADSN" adsnDefault = "lxa.db" ) const ( HashStrategySha2_256 = "SHA2_256" HashStrategySha3_256 = "SHA3_256" HashStrategySha3_512 = "SHA3_512" ) // type UsagePolicy struct { // Name string `json:"name"` // Noun string `json:"noun"` // Verb string `json:"verb"` // Subject string `json:"subject"` // Extra string `json:"extra"` // ExtraType string `json:"extra_type"` // Windows string `json:"windows"` // } type AddrChange struct { Address string Port uint16 } type Merchant struct { Name string `json:"name"` ApiUrl string `json:"api_url"` TierPlanIds map[string]string `json:"tier_plan_ids"` PlanIdTiers map[string]string `json:"plan_id_tiers"` } type BusinessTier struct { Name string `json:"default"` DisplayName string `json:"display_names"` // mostly for UI ordering, do not try to infer other things FeatureLevel int `json:"feature_level"` BackhalvesAnonymous int `json:"backhalves_anonymous"` BackhalvesNamed int `json:"backhalves_named"` Folders int `json:"folders"` AnalyticsRetention int `json:"analytics_retention"` AnalyticsMaxGranularity int `json:"analytics_max_granularity"` AnalyticsViewLimit int `json:"analytics_view_limit"` PriceByYear int `json:"price_by_year"` PriceByMonth int `json:"price_by_month"` } type Config struct { Title string `json:"title"` SupportEmail string `json:"support_email"` SupportPhone string `json:"support_phone"` Purpose string `json:"purpose"` Disclaimer string `json:"disclaimer"` MaxDefaultBusinesses int `json:"max_default_businesses"` SignupNeedsApprove bool `json:"signup_needs_approve"` ListenAddress string `json:"listen_address"` Port uint16 `json:"port"` SessionLengthMax int `json:"session_length_max"` EmailConfirm bool `json:"email_confirm"` EmailRegex string `json:"email_regex"` EmailRequired bool `json:"email_required"` PasswordSaltLength int `json:"password_salt_length"` PasswordKeyLength int `json:"password_key_length"` PasswordRounds int `json:"password_rounds"` PasswordHashStrategy string `json:"password_hash_strategy"` PasswordAttempts int `json:"password_attempts"` PasswordMin int `json:"password_min"` PasswordRegex string `json:"password_regex"` PasswordHelp string `json:"password_help"` PasswordRequired bool `json:"password_required"` LogAddSource bool `json:"log_add_source"` LogLevel string `json:"log_level"` LogLevelKey string `json:"log_level_key"` LogLevelOverrides string `json:"log_level_overrides"` LogTimeFormat string `json:"log_time_format"` LogTimeKey string `json:"log_time_key"` AnalyticsDisable bool `json:"analytics_disable"` AnalyticsWriteTimeout int `json:"analytics_write_timeout"` AnalyticsReadTimeout int `json:"analytics_read_timeout"` ConfigReloadSeconds uint `json:"config_reload_seconds"` PersonStyleTextColor string `json:"person_style_text_color"` PersonStyleTextDarkColor string `json:"person_style_text_dark_color"` PersonStyleBackgroundColor string `json:"person_style_background_color"` PersonStyleBackgroundDarkColor string `json:"person_style_background_dark_color"` MerchantDefault string `json:"merchant_default"` BusinessTiers map[string]*BusinessTier `json:"business_tiers"` Merchants map[string]*Merchant `json:"merchants"` CashAccept bool `json:"cash_accept"` CashRoundDirection bool `json:"cash_round_direction"` // business tier prices are represented in 100ths of pennies, USD // so to have cash as default merchant but only available yearly, // and rounded down by $5 // { // "cash_accept": true, "cash_round_direction": false, "cash_round_quantity": 50000, // "merchants": { // "name": "cash", // "tier_plan_ids": {"start": "cash-plan-id-start", "medium": "cash-plan-id-medium", "high": "cash-plan-id-high"}, // "plan_id_tiers": {"cash-plan-id-start": "start", "cash-plan-id-medium": "medium", "cash-plan-id-high": "high"} // } // } CashRoundQuantity int `json:"cash_round_quantity"` // UsagePolicies map[string]*UsagePolicy `json:"usage_policies"` } func setStr(f url.Values, cols *[]string, vals *[]any, name string, current string) { if val := f.Get(name); val != current { *cols = append(*cols, name) *vals = append(*vals, val) } } func setBoo(f url.Values, cols *[]string, vals *[]any, name string, current bool) { valStr := f.Get(name) val := valStr == "on" if val != current { *cols = append(*cols, name) *vals = append(*vals, val) } } func setEnm[T comparable](f url.Values, cols *[]string, vals *[]any, name string, current T, oks ...T) { valStr := f.Get(name) var snt any = current var val T switch snt.(type) { // booleans, numbers, strings, pointers, case string: var valStrAny any = valStr val = valStrAny.(T) case int: valInt, err := strconv.Atoi(valStr) if err != nil { return } var valStrAny any = valInt val = valStrAny.(T) case uint16: valInt, err := strconv.Atoi(valStr) if err != nil { return } var valStrAny any = uint16(valInt) val = valStrAny.(T) default: return } if val == current { return } found := slices.Contains(oks, val) if !found { return } *cols = append(*cols, name) *vals = append(*vals, val) } func setNum[T int | uint16 | uint](f url.Values, cols *[]string, vals *[]any, name string, current T) error { valStr := f.Get(name) val, err := strconv.Atoi(valStr) if err != nil { return err } valTyp := T(val) if valTyp != current { *cols = append(*cols, name) *vals = append(*vals, val) } return nil } func ParseUpdateForm(f url.Values, c *Config) ([]string, []any, error) { cols := []string{} vals := []any{} setStr(f, &cols, &vals, "title", c.Title) setStr(f, &cols, &vals, "support_email", c.SupportEmail) setStr(f, &cols, &vals, "support_phone", c.SupportPhone) setStr(f, &cols, &vals, "purpose", c.Purpose) setStr(f, &cols, &vals, "disclaimer", c.Disclaimer) setNum(f, &cols, &vals, "max_default_businesses", c.MaxDefaultBusinesses) setStr(f, &cols, &vals, "listen_address", c.ListenAddress) setNum(f, &cols, &vals, "port", c.Port) setBoo(f, &cols, &vals, "signup_needs_approve", c.SignupNeedsApprove) setNum(f, &cols, &vals, "session_length_max", c.SessionLengthMax) setBoo(f, &cols, &vals, "email_confirm", c.EmailConfirm) setBoo(f, &cols, &vals, "email_required", c.EmailRequired) setStr(f, &cols, &vals, "email_regex", c.EmailRegex) setNum(f, &cols, &vals, "password_salt_length", c.PasswordSaltLength) setNum(f, &cols, &vals, "password_key_length", c.PasswordKeyLength) setNum(f, &cols, &vals, "password_rounds", c.PasswordRounds) setEnm(f, &cols, &vals, "password_hash_strategy", c.PasswordHashStrategy, HashStrategySha2_256, HashStrategySha3_256, HashStrategySha3_512) setNum(f, &cols, &vals, "password_attempts", c.PasswordAttempts) setNum(f, &cols, &vals, "password_min", c.PasswordMin) setStr(f, &cols, &vals, "password_regex", c.PasswordRegex) setStr(f, &cols, &vals, "password_help", c.PasswordHelp) setStr(f, &cols, &vals, "log_level", c.LogLevel) setStr(f, &cols, &vals, "log_level_key", c.LogLevelKey) setStr(f, &cols, &vals, "log_level_overrides", c.LogLevelOverrides) setStr(f, &cols, &vals, "log_time_format", c.LogTimeFormat) setStr(f, &cols, &vals, "log_time_key", c.LogTimeKey) setBoo(f, &cols, &vals, "log_add_source", c.LogAddSource) setBoo(f, &cols, &vals, "analytics_disable", c.AnalyticsDisable) setNum(f, &cols, &vals, "analytics_write_timeout", c.AnalyticsWriteTimeout) setNum(f, &cols, &vals, "analytics_read_timeout", c.AnalyticsReadTimeout) setStr(f, &cols, &vals, "person_style_text_color", c.PersonStyleTextColor) setStr(f, &cols, &vals, "person_style_text_dark_color", c.PersonStyleTextDarkColor) setStr(f, &cols, &vals, "person_style_background_color", c.PersonStyleBackgroundColor) setStr(f, &cols, &vals, "person_style_background_dark_color", c.PersonStyleBackgroundDarkColor) setStr(f, &cols, &vals, "merchant_default", c.MerchantDefault) setNum(f, &cols, &vals, "config_reload_seconds", c.ConfigReloadSeconds) return cols, vals, nil } func GetDsn() string { dsnStrP := flag.String("dsn", dsnDefault, "path to sqlite db file. If not set, try to read LX_DSN env var. If not set, default value used") flag.Parse() if dsnStrP == nil { os.Exit(1) } dsnStr := *dsnStrP if dsnStr == dsnDefault { if osDsn := os.Getenv(dsnOsStr); osDsn != "" { dsnStr = osDsn } } return dsnStr } func GetAdsn() string { dsnStrP := flag.String("adsn", adsnDefault, "path to sqlite db file for analytics. If not set, try to read LX_ADSN env var. If not set, default value used") flag.Parse() if dsnStrP == nil { os.Exit(1) } dsnStr := *dsnStrP if dsnStr == adsnDefault { if osDsn := os.Getenv(adsnOsStr); osDsn != "" { dsnStr = osDsn } } return dsnStr }