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
}