diff --git a/config/config.go b/config/config.go index 5ca1358..ea8754f 100644 --- a/config/config.go +++ b/config/config.go @@ -1,97 +1,97 @@ package config import ( "gopkg.in/ini.v1" ) const ( FileName = "config.ini" ) type ( ServerCfg struct { - Host string `ini:"host"` Port int `ini:"port"` } DatabaseCfg struct { Type string `ini:"type"` 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"` + 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"` // 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{ Server: ServerCfg{ - Host: "http://localhost:8080", Port: 8080, }, Database: DatabaseCfg{ Type: "mysql", Host: "localhost", Port: 3306, }, App: AppCfg{ + Host: "http://localhost:8080", Theme: "write", WebFonts: true, SingleUser: true, MinUsernameLen: 3, Federation: true, PublicStats: true, }, } } 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 3eabce2..5ba2053 100644 --- a/config/setup.go +++ b/config/setup.go @@ -1,181 +1,181 @@ package config import ( "fmt" "github.com/fatih/color" "github.com/manifoldco/promptui" "github.com/mitchellh/go-wordwrap" "strconv" ) func Configure() error { c, err := Load() if err != nil { fmt.Println("No configuration yet. Creating new.") c = New() } else { fmt.Println("Configuration loaded.") } title := color.New(color.Bold, color.BgGreen).PrintlnFunc() 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 generate 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() prompt := promptui.Prompt{ Label: "Local port", Validate: validatePort, Default: fmt.Sprintf("%d", c.Server.Port), } port, err := prompt.Run() if err != nil { return err } c.Server.Port, _ = strconv.Atoi(port) // Ignore error, as we've already validated number prompt = promptui.Prompt{ Label: "Public-facing host", Validate: validateDomain, - Default: c.Server.Host, + Default: c.App.Host, } - c.Server.Host, err = prompt.Run() + c.App.Host, err = prompt.Run() if err != nil { return err } fmt.Println() title(" Database setup ") fmt.Println() prompt = promptui.Prompt{ Label: "Username", Validate: validateNonEmpty, Default: c.Database.User, } c.Database.User, err = prompt.Run() if err != nil { return err } prompt = promptui.Prompt{ Label: "Password", Validate: validateNonEmpty, Default: c.Database.Password, Mask: '*', } c.Database.Password, err = prompt.Run() if err != nil { return err } prompt = promptui.Prompt{ Label: "Database name", Validate: validateNonEmpty, Default: c.Database.Database, } c.Database.Database, err = prompt.Run() if err != nil { return err } prompt = promptui.Prompt{ Label: "Host", Validate: validateNonEmpty, Default: c.Database.Host, } c.Database.Host, err = prompt.Run() if err != nil { return err } prompt = promptui.Prompt{ Label: "Port", Validate: validatePort, Default: fmt.Sprintf("%d", c.Database.Port), } dbPort, err := prompt.Run() if err != nil { return err } c.Database.Port, _ = strconv.Atoi(dbPort) // Ignore error, as we've already validated number fmt.Println() title(" App setup ") fmt.Println() selPrompt := promptui.Select{ Label: "Site type", Items: []string{"Single user", "Multiple users"}, } _, usersType, err := selPrompt.Run() if err != nil { return err } c.App.SingleUser = usersType == "Single user" siteNameLabel := "Instance name" if c.App.SingleUser { siteNameLabel = "Blog name" } prompt = promptui.Prompt{ Label: siteNameLabel, Validate: validateNonEmpty, Default: c.App.SiteName, } c.App.SiteName, err = prompt.Run() if err != nil { return err } if !c.App.SingleUser { selPrompt = promptui.Select{ Label: "Registration", Items: []string{"Open", "Closed"}, } _, regType, err := selPrompt.Run() if err != nil { return err } c.App.OpenRegistration = regType == "Open" } selPrompt = promptui.Select{ Label: "Federation", Items: []string{"Enabled", "Disabled"}, } _, fedType, err := selPrompt.Run() if err != nil { return err } c.App.Federation = fedType == "Enabled" if c.App.Federation { selPrompt = promptui.Select{ Label: "Federation usage stats", Items: []string{"Public", "Private"}, } _, fedStatsType, err := selPrompt.Run() if err != nil { return err } c.App.PublicStats = fedStatsType == "Public" selPrompt = promptui.Select{ Label: "Instance metadata privacy", Items: []string{"Public", "Private"}, } _, fedStatsType, err = selPrompt.Run() if err != nil { return err } c.App.Private = fedStatsType == "Private" } return Save(c) } diff --git a/nodeinfo.go b/nodeinfo.go index ae50e9f..3318fb4 100644 --- a/nodeinfo.go +++ b/nodeinfo.go @@ -1,87 +1,87 @@ package writefreely import ( "fmt" "github.com/writeas/go-nodeinfo" "github.com/writeas/web-core/log" "github.com/writeas/writefreely/config" ) type nodeInfoResolver struct { cfg *config.Config db *datastore } func nodeInfoConfig(cfg *config.Config) *nodeinfo.Config { name := cfg.App.SiteName return &nodeinfo.Config{ - BaseURL: cfg.Server.Host, + BaseURL: cfg.App.Host, InfoURL: "/api/nodeinfo", Metadata: nodeinfo.Metadata{ NodeName: name, NodeDescription: "Minimal, federated blogging platform.", Private: cfg.App.Private, Software: nodeinfo.SoftwareMeta{ HomePage: softwareURL, GitHub: "https://github.com/writeas/writefreely", Follow: "https://writing.exchange/@write_as", }, }, Protocols: []nodeinfo.NodeProtocol{ nodeinfo.ProtocolActivityPub, }, Services: nodeinfo.Services{ Inbound: []nodeinfo.NodeService{}, Outbound: []nodeinfo.NodeService{}, }, Software: nodeinfo.SoftwareInfo{ Name: serverSoftware, Version: softwareVer, }, } } func (r nodeInfoResolver) IsOpenRegistration() (bool, error) { return !r.cfg.App.Private, nil } func (r nodeInfoResolver) Usage() (nodeinfo.Usage, error) { var collCount, postCount, activeHalfYear, activeMonth int err := r.db.QueryRow(`SELECT COUNT(*) FROM collections`).Scan(&collCount) if err != nil { collCount = 0 } err = r.db.QueryRow(`SELECT COUNT(*) FROM posts`).Scan(&postCount) if err != nil { log.Error("Unable to fetch post counts: %v", err) } if r.cfg.App.PublicStats { // Display bi-yearly / monthly stats err = r.db.QueryRow(fmt.Sprintf(`SELECT COUNT(*) FROM ( SELECT DISTINCT collection_id FROM posts INNER JOIN collections c ON collection_id = c.id WHERE collection_id IS NOT NULL AND updated > DATE_SUB(NOW(), INTERVAL 6 MONTH)) co`, CollPublic)).Scan(&activeHalfYear) err = r.db.QueryRow(fmt.Sprintf(`SELECT COUNT(*) FROM ( SELECT DISTINCT collection_id FROM posts INNER JOIN FROM collections c ON collection_id = c.id WHERE collection_id IS NOT NULL AND updated > DATE_SUB(NOW(), INTERVAL 1 MONTH)) co`, CollPublic)).Scan(&activeMonth) } return nodeinfo.Usage{ Users: nodeinfo.UsageUsers{ Total: collCount, ActiveHalfYear: activeHalfYear, ActiveMonth: activeMonth, }, LocalPosts: postCount, }, nil } diff --git a/routes.go b/routes.go index 45e3be9..fcc9d1c 100644 --- a/routes.go +++ b/routes.go @@ -1,41 +1,40 @@ package writefreely import ( "github.com/gorilla/mux" "github.com/writeas/go-nodeinfo" "github.com/writeas/web-core/log" "github.com/writeas/writefreely/config" "net/http" "strings" ) func initRoutes(handler *Handler, r *mux.Router, cfg *config.Config, db *datastore) { isSingleUser := !cfg.App.MultiUser - // Write.as router - hostSubroute := cfg.Server.Host[strings.Index(cfg.Server.Host, "://")+3:] + hostSubroute := cfg.App.Host[strings.Index(cfg.App.Host, "://")+3:] if isSingleUser { hostSubroute = "{domain}" } else { if strings.HasPrefix(hostSubroute, "localhost") { hostSubroute = "localhost" } } if isSingleUser { log.Info("Adding %s routes (single user)...", hostSubroute) return } // Primary app routes log.Info("Adding %s routes (multi-user)...", hostSubroute) write := r.Host(hostSubroute).Subrouter() // Federation endpoints // nodeinfo niCfg := nodeInfoConfig(cfg) ni := nodeinfo.NewService(*niCfg, nodeInfoResolver{cfg, db}) write.HandleFunc(nodeinfo.NodeInfoPath, handler.LogHandlerFunc(http.HandlerFunc(ni.NodeInfoDiscover))) write.HandleFunc(niCfg.InfoURL, handler.LogHandlerFunc(http.HandlerFunc(ni.NodeInfo))) } diff --git a/session.go b/session.go index bd768cb..931b87b 100644 --- a/session.go +++ b/session.go @@ -1,126 +1,126 @@ package writefreely import ( "encoding/gob" "github.com/gorilla/sessions" "github.com/writeas/web-core/log" "net/http" "strings" ) const ( day = 86400 sessionLength = 180 * day cookieName = "wfu" cookieUserVal = "u" ) // initSession creates the cookie store. It depends on the keychain already // being loaded. func initSession(app *app) *sessions.CookieStore { // Register complex data types we'll be storing in cookies gob.Register(&User{}) // Create the cookie store store := sessions.NewCookieStore(app.keys.cookieAuthKey, app.keys.cookieKey) store.Options = &sessions.Options{ Path: "/", MaxAge: sessionLength, HttpOnly: true, - Secure: strings.HasPrefix(app.cfg.Server.Host, "https://"), + Secure: strings.HasPrefix(app.cfg.App.Host, "https://"), } return store } func getSessionFlashes(app *app, w http.ResponseWriter, r *http.Request, session *sessions.Session) ([]string, error) { var err error if session == nil { session, err = app.sessionStore.Get(r, cookieName) if err != nil { return nil, err } } f := []string{} if flashes := session.Flashes(); len(flashes) > 0 { for _, flash := range flashes { if str, ok := flash.(string); ok { f = append(f, str) } } } saveUserSession(app, r, w) return f, nil } func addSessionFlash(app *app, w http.ResponseWriter, r *http.Request, m string, session *sessions.Session) error { var err error if session == nil { session, err = app.sessionStore.Get(r, cookieName) } if err != nil { log.Error("Unable to add flash '%s': %v", m, err) return err } session.AddFlash(m) saveUserSession(app, r, w) return nil } func getUserAndSession(app *app, r *http.Request) (*User, *sessions.Session) { session, err := app.sessionStore.Get(r, cookieName) if err == nil { // Got the currently logged-in user val := session.Values[cookieUserVal] var u = &User{} var ok bool if u, ok = val.(*User); ok { return u, session } } return nil, nil } func getUserSession(app *app, r *http.Request) *User { u, _ := getUserAndSession(app, r) return u } func saveUserSession(app *app, r *http.Request, w http.ResponseWriter) error { session, err := app.sessionStore.Get(r, cookieName) if err != nil { return ErrInternalCookieSession } // Extend the session session.Options.MaxAge = int(sessionLength) // Remove any information that accidentally got added // FIXME: find where Plan information is getting saved to cookie. val := session.Values[cookieUserVal] var u = &User{} var ok bool if u, ok = val.(*User); ok { session.Values[cookieUserVal] = u.Cookie() } err = session.Save(r, w) if err != nil { log.Error("Couldn't saveUserSession: %v", err) } return err } func getFullUserSession(app *app, r *http.Request) *User { u := getUserSession(app, r) if u == nil { return nil } u, _ = app.db.GetUserByID(u.ID) return u }