diff --git a/config/config.go b/config/config.go index fad258d..ab7cea3 100644 --- a/config/config.go +++ b/config/config.go @@ -1,113 +1,127 @@ package config import ( "gopkg.in/ini.v1" ) const ( FileName = "config.ini" ) type ( ServerCfg struct { HiddenHost string `ini:"hidden_host"` Port int `ini:"port"` Bind string `ini:"bind"` TLSCertPath string `ini:"tls_cert_path"` TLSKeyPath string `ini:"tls_key_path"` Dev bool `ini:"-"` } DatabaseCfg struct { Type string `ini:"type"` FileName string `ini:"filename"` User string `ini:"username"` Password string `ini:"password"` Database string `ini:"database"` Host string `ini:"host"` Port int `ini:"port"` } AppCfg struct { SiteName string `ini:"site_name"` SiteDesc string `ini:"site_description"` Host string `ini:"host"` // Site appearance Theme string `ini:"theme"` JSDisabled bool `ini:"disable_js"` WebFonts bool `ini:"webfonts"` // Users SingleUser bool `ini:"single_user"` OpenRegistration bool `ini:"open_registration"` MinUsernameLen int `ini:"min_username_len"` MaxBlogs int `ini:"max_blogs"` // Federation Federation bool `ini:"federation"` PublicStats bool `ini:"public_stats"` Private bool `ini:"private"` } Config struct { Server ServerCfg `ini:"server"` Database DatabaseCfg `ini:"database"` App AppCfg `ini:"app"` } ) func New() *Config { - return &Config{ + c := &Config{ Server: ServerCfg{ Port: 8080, Bind: "localhost", /* IPV6 support when not using localhost? */ }, - Database: DatabaseCfg{ - Type: "mysql", - Host: "localhost", - Port: 3306, - }, App: AppCfg{ Host: "http://localhost:8080", Theme: "write", WebFonts: true, SingleUser: true, MinUsernameLen: 3, MaxBlogs: 1, Federation: true, PublicStats: true, }, } + c.UseMySQL(true) + return c +} + +// UseMySQL resets the Config's Database to use default values for a MySQL setup. +func (cfg *Config) UseMySQL(fresh bool) { + cfg.Database.Type = "mysql" + if fresh { + cfg.Database.Host = "localhost" + cfg.Database.Port = 3306 + } +} + +// UseSQLite resets the Config's Database to use default values for a SQLite setup. +func (cfg *Config) UseSQLite(fresh bool) { + cfg.Database.Type = "sqlite3" + if fresh { + cfg.Database.FileName = "writefreely.db" + } } func (cfg *Config) IsSecureStandalone() bool { return cfg.Server.Port == 443 && cfg.Server.TLSCertPath != "" && cfg.Server.TLSKeyPath != "" } func Load() (*Config, error) { cfg, err := ini.Load(FileName) if err != nil { return nil, err } // Parse INI file uc := &Config{} err = cfg.MapTo(uc) if err != nil { return nil, err } return uc, nil } func Save(uc *Config) error { cfg := ini.Empty() err := ini.ReflectFrom(cfg, uc) if err != nil { return err } return cfg.SaveTo(FileName) } diff --git a/config/setup.go b/config/setup.go index 6a2f780..d086500 100644 --- a/config/setup.go +++ b/config/setup.go @@ -1,318 +1,349 @@ package config import ( "fmt" "github.com/fatih/color" "github.com/manifoldco/promptui" "github.com/mitchellh/go-wordwrap" "github.com/writeas/web-core/auth" "strconv" ) type SetupData struct { User *UserCreation Config *Config } func Configure() (*SetupData, error) { data := &SetupData{} var err error data.Config, err = Load() var action string + isNewCfg := false if err != nil { fmt.Println("No configuration yet. Creating new.") data.Config = New() action = "generate" + isNewCfg = true } else { fmt.Println("Configuration loaded.") action = "update" } title := color.New(color.Bold, color.BgGreen).PrintFunc() intro := color.New(color.Bold, color.FgWhite).PrintlnFunc() fmt.Println() intro(" ✍ Write Freely Configuration ✍") fmt.Println() fmt.Println(wordwrap.WrapString(" This quick configuration process will "+action+" the application's config file, "+FileName+".\n\n It validates your input along the way, so you can be sure any future errors aren't caused by a bad configuration. If you'd rather configure your server manually, instead run: writefreely --create-config and edit that file.", 75)) fmt.Println() title(" Server setup ") fmt.Println() tmpls := &promptui.PromptTemplates{ Success: "{{ . | bold | faint }}: ", } selTmpls := &promptui.SelectTemplates{ Selected: fmt.Sprintf(`{{.Label}} {{ . | faint }}`), } // Environment selection selPrompt := promptui.Select{ Templates: selTmpls, Label: "Environment", Items: []string{"Development", "Production, standalone", "Production, behind reverse proxy"}, } _, envType, err := selPrompt.Run() if err != nil { return data, err } isDevEnv := envType == "Development" isStandalone := envType == "Production, standalone" data.Config.Server.Dev = isDevEnv var prompt promptui.Prompt if isDevEnv || !isStandalone { // Running in dev environment or behind reverse proxy; ask for port prompt = promptui.Prompt{ Templates: tmpls, Label: "Local port", Validate: validatePort, Default: fmt.Sprintf("%d", data.Config.Server.Port), } port, err := prompt.Run() if err != nil { return data, err } data.Config.Server.Port, _ = strconv.Atoi(port) // Ignore error, as we've already validated number } if isStandalone { selPrompt = promptui.Select{ Templates: selTmpls, Label: "Web server mode", Items: []string{"Insecure (port 80)", "Secure (port 443)"}, } sel, _, err := selPrompt.Run() if err != nil { return data, err } if sel == 0 { data.Config.Server.Port = 80 data.Config.Server.TLSCertPath = "" data.Config.Server.TLSKeyPath = "" } else if sel == 1 { data.Config.Server.Port = 443 prompt = promptui.Prompt{ Templates: tmpls, Label: "Certificate path", Validate: validateNonEmpty, Default: data.Config.Server.TLSCertPath, } data.Config.Server.TLSCertPath, err = prompt.Run() if err != nil { return data, err } prompt = promptui.Prompt{ Templates: tmpls, Label: "Key path", Validate: validateNonEmpty, Default: data.Config.Server.TLSKeyPath, } data.Config.Server.TLSKeyPath, err = prompt.Run() if err != nil { return data, err } } } else { data.Config.Server.TLSCertPath = "" data.Config.Server.TLSKeyPath = "" } fmt.Println() title(" Database setup ") fmt.Println() - prompt = promptui.Prompt{ - Templates: tmpls, - Label: "Username", - Validate: validateNonEmpty, - Default: data.Config.Database.User, + selPrompt = promptui.Select{ + Templates: selTmpls, + Label: "Database driver", + Items: []string{"MySQL", "SQLite"}, } - data.Config.Database.User, err = prompt.Run() + sel, _, err := selPrompt.Run() if err != nil { return data, err } - prompt = promptui.Prompt{ - Templates: tmpls, - Label: "Password", - Validate: validateNonEmpty, - Default: data.Config.Database.Password, - Mask: '*', - } - data.Config.Database.Password, err = prompt.Run() - if err != nil { - return data, err - } + if sel == 0 { + // Configure for MySQL + data.Config.UseMySQL(isNewCfg) - prompt = promptui.Prompt{ - Templates: tmpls, - Label: "Database name", - Validate: validateNonEmpty, - Default: data.Config.Database.Database, - } - data.Config.Database.Database, err = prompt.Run() - if err != nil { - return data, err - } + prompt = promptui.Prompt{ + Templates: tmpls, + Label: "Username", + Validate: validateNonEmpty, + Default: data.Config.Database.User, + } + data.Config.Database.User, err = prompt.Run() + if err != nil { + return data, err + } - prompt = promptui.Prompt{ - Templates: tmpls, - Label: "Host", - Validate: validateNonEmpty, - Default: data.Config.Database.Host, - } - data.Config.Database.Host, err = prompt.Run() - if err != nil { - return data, err - } + prompt = promptui.Prompt{ + Templates: tmpls, + Label: "Password", + Validate: validateNonEmpty, + Default: data.Config.Database.Password, + Mask: '*', + } + data.Config.Database.Password, err = prompt.Run() + if err != nil { + return data, err + } - prompt = promptui.Prompt{ - Templates: tmpls, - Label: "Port", - Validate: validatePort, - Default: fmt.Sprintf("%d", data.Config.Database.Port), - } - dbPort, err := prompt.Run() - if err != nil { - return data, err + prompt = promptui.Prompt{ + Templates: tmpls, + Label: "Database name", + Validate: validateNonEmpty, + Default: data.Config.Database.Database, + } + data.Config.Database.Database, err = prompt.Run() + if err != nil { + return data, err + } + + prompt = promptui.Prompt{ + Templates: tmpls, + Label: "Host", + Validate: validateNonEmpty, + Default: data.Config.Database.Host, + } + data.Config.Database.Host, err = prompt.Run() + if err != nil { + return data, err + } + + prompt = promptui.Prompt{ + Templates: tmpls, + Label: "Port", + Validate: validatePort, + Default: fmt.Sprintf("%d", data.Config.Database.Port), + } + dbPort, err := prompt.Run() + if err != nil { + return data, err + } + data.Config.Database.Port, _ = strconv.Atoi(dbPort) // Ignore error, as we've already validated number + } else if sel == 1 { + // Configure for SQLite + data.Config.UseSQLite(isNewCfg) + + prompt = promptui.Prompt{ + Templates: tmpls, + Label: "Filename", + Validate: validateNonEmpty, + Default: data.Config.Database.FileName, + } + data.Config.Database.FileName, err = prompt.Run() + if err != nil { + return data, err + } } - data.Config.Database.Port, _ = strconv.Atoi(dbPort) // Ignore error, as we've already validated number fmt.Println() title(" App setup ") fmt.Println() selPrompt = promptui.Select{ Templates: selTmpls, Label: "Site type", Items: []string{"Single user blog", "Multi-user instance"}, } _, usersType, err := selPrompt.Run() if err != nil { return data, err } data.Config.App.SingleUser = usersType == "Single user blog" if data.Config.App.SingleUser { data.User = &UserCreation{} // prompt for username prompt = promptui.Prompt{ Templates: tmpls, Label: "Admin username", Validate: validateNonEmpty, } data.User.Username, err = prompt.Run() if err != nil { return data, err } // prompt for password prompt = promptui.Prompt{ Templates: tmpls, Label: "Admin password", Validate: validateNonEmpty, } newUserPass, err := prompt.Run() if err != nil { return data, err } data.User.HashedPass, err = auth.HashPass([]byte(newUserPass)) if err != nil { return data, err } } siteNameLabel := "Instance name" if data.Config.App.SingleUser { siteNameLabel = "Blog name" } prompt = promptui.Prompt{ Templates: tmpls, Label: siteNameLabel, Validate: validateNonEmpty, Default: data.Config.App.SiteName, } data.Config.App.SiteName, err = prompt.Run() if err != nil { return data, err } prompt = promptui.Prompt{ Templates: tmpls, Label: "Public URL", Validate: validateDomain, Default: data.Config.App.Host, } data.Config.App.Host, err = prompt.Run() if err != nil { return data, err } if !data.Config.App.SingleUser { selPrompt = promptui.Select{ Templates: selTmpls, Label: "Registration", Items: []string{"Open", "Closed"}, } _, regType, err := selPrompt.Run() if err != nil { return data, err } data.Config.App.OpenRegistration = regType == "Open" prompt = promptui.Prompt{ Templates: tmpls, Label: "Max blogs per user", Default: fmt.Sprintf("%d", data.Config.App.MaxBlogs), } maxBlogs, err := prompt.Run() if err != nil { return data, err } data.Config.App.MaxBlogs, _ = strconv.Atoi(maxBlogs) // Ignore error, as we've already validated number } selPrompt = promptui.Select{ Templates: selTmpls, Label: "Federation", Items: []string{"Enabled", "Disabled"}, } _, fedType, err := selPrompt.Run() if err != nil { return data, err } data.Config.App.Federation = fedType == "Enabled" if data.Config.App.Federation { selPrompt = promptui.Select{ Templates: selTmpls, Label: "Federation usage stats", Items: []string{"Public", "Private"}, } _, fedStatsType, err := selPrompt.Run() if err != nil { return data, err } data.Config.App.PublicStats = fedStatsType == "Public" selPrompt = promptui.Select{ Templates: selTmpls, Label: "Instance metadata privacy", Items: []string{"Public", "Private"}, } _, fedStatsType, err = selPrompt.Run() if err != nil { return data, err } data.Config.App.Private = fedStatsType == "Private" } return data, Save(data.Config) }