From fe84b0c4f161f003eee4d20c3e4531e65d98a2b8 Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Tue, 7 Nov 2017 19:48:22 -0800 Subject: [PATCH] Init site config DB, login required middleware --- core/admin.go | 16 ++++++ core/app.go | 27 ++++++++-- core/auth.go | 43 ++++++---------- core/middleware.go | 40 +++++++++++++++ core/models/settings/settings.go | 84 ++++++++++++++++++++++++++++++++ core/models/users/users.go | 6 +-- core/templates.go | 16 +++++- root/.layout.gohtml | 9 ++++ 8 files changed, 205 insertions(+), 36 deletions(-) create mode 100644 core/middleware.go create mode 100644 core/models/settings/settings.go diff --git a/core/admin.go b/core/admin.go index f06a068..26ff2e2 100644 --- a/core/admin.go +++ b/core/admin.go @@ -3,10 +3,17 @@ package core import ( "net/http" + "github.com/gorilla/sessions" "github.com/kirsle/blog/core/forms" + "github.com/kirsle/blog/core/models/settings" "github.com/kirsle/blog/core/models/users" ) +// AdminHandler is the admin landing page. +func (b *Blog) AdminHandler(w http.ResponseWriter, r *http.Request) { + b.RenderTemplate(w, r, "admin/index", nil) +} + // SetupHandler is the initial blog setup route. func (b *Blog) SetupHandler(w http.ResponseWriter, r *http.Request) { vars := &Vars{ @@ -24,6 +31,14 @@ func (b *Blog) SetupHandler(w http.ResponseWriter, r *http.Request) { if err != nil { vars.Error = err } else { + // Save the site config. + log.Info("Creating default website config file") + s := settings.Defaults() + s.Save() + + // Re-initialize the cookie store with the new secret key. + b.store = sessions.NewCookieStore([]byte(s.Security.SecretKey)) + log.Info("Creating admin account %s", form.Username) user := &users.User{ Username: form.Username, @@ -38,6 +53,7 @@ func (b *Blog) SetupHandler(w http.ResponseWriter, r *http.Request) { } // All set! + b.Login(w, r, user) b.Redirect(w, "/admin") return } diff --git a/core/app.go b/core/app.go index 89dac3f..058c4aa 100644 --- a/core/app.go +++ b/core/app.go @@ -7,6 +7,7 @@ import ( "github.com/gorilla/mux" "github.com/gorilla/sessions" "github.com/kirsle/blog/core/jsondb" + "github.com/kirsle/blog/core/models/settings" "github.com/kirsle/blog/core/models/users" "github.com/urfave/negroni" ) @@ -34,16 +35,36 @@ func New(documentRoot, userRoot string) *Blog { DocumentRoot: documentRoot, UserRoot: userRoot, DB: jsondb.New(filepath.Join(userRoot, ".private")), - - store: sessions.NewCookieStore([]byte("secret-key")), // TODO configurable! } - // Initialize all the models. + // Load the site config, or start with defaults if not found. + settings.DB = blog.DB + config, err := settings.Load() + if err != nil { + config = settings.Defaults() + } + + // Initialize the session cookie store. + blog.store = sessions.NewCookieStore([]byte(config.Security.SecretKey)) + users.HashCost = config.Security.HashCost + + // Initialize the rest of the models. users.DB = blog.DB + // Initialize the router. r := mux.NewRouter() blog.r = r + + // Blog setup. r.HandleFunc("/admin/setup", blog.SetupHandler) + + // Admin pages that require a logged-in user. + admin := mux.NewRouter() + admin.HandleFunc("/admin", blog.AdminHandler) + r.PathPrefix("/admin").Handler(negroni.New( + negroni.HandlerFunc(blog.LoginRequired), + negroni.Wrap(admin), + )) r.HandleFunc("/login", blog.LoginHandler) r.HandleFunc("/logout", blog.LogoutHandler) r.HandleFunc("/", blog.PageHandler) diff --git a/core/auth.go b/core/auth.go index c603d2c..c92a28f 100644 --- a/core/auth.go +++ b/core/auth.go @@ -1,7 +1,6 @@ package core import ( - "context" "errors" "net/http" @@ -15,24 +14,16 @@ const ( userKey key = iota ) -// AuthMiddleware loads the user's authentication state. -func (b *Blog) AuthMiddleware(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { - session, _ := b.store.Get(r, "session") - log.Info("Session: %v", session.Values) - if loggedIn, ok := session.Values["logged-in"].(bool); ok && loggedIn { - // They seem to be logged in. Get their user object. - id := session.Values["user-id"].(int) - u, err := users.Load(id) - if err != nil { - log.Error("Error loading user ID %d from session: %v", id, err) - next(w, r) - return - } - - ctx := context.WithValue(r.Context(), userKey, u) - next(w, r.WithContext(ctx)) +// Login logs the browser in as the given user. +func (b *Blog) Login(w http.ResponseWriter, r *http.Request, u *users.User) error { + session, err := b.store.Get(r, "session") // TODO session name + if err != nil { + return err } - next(w, r) + session.Values["logged-in"] = true + session.Values["user-id"] = u.ID + session.Save(r, w) + return nil } // LoginHandler shows and handles the login page. @@ -58,18 +49,16 @@ func (b *Blog) LoginHandler(w http.ResponseWriter, r *http.Request) { } else { // Login OK! vars.Flash = "Login OK!" + b.Login(w, r, user) - // Log in the user. - session, err := b.store.Get(r, "session") // TODO session name - if err != nil { - vars.Error = err + // A next URL given? TODO: actually get to work + next := r.FormValue("next") + log.Info("Redirect after login to: %s", next) + if len(next) > 0 && next[0] == '/' { + b.Redirect(w, next) } else { - session.Values["logged-in"] = true - session.Values["user-id"] = user.ID - session.Save(r, w) + b.Redirect(w, "/") } - - b.Redirect(w, "/login") return } } diff --git a/core/middleware.go b/core/middleware.go new file mode 100644 index 0000000..ddbfd9f --- /dev/null +++ b/core/middleware.go @@ -0,0 +1,40 @@ +package core + +import ( + "context" + "net/http" + + "github.com/kirsle/blog/core/models/users" +) + +// AuthMiddleware loads the user's authentication state. +func (b *Blog) AuthMiddleware(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { + session, _ := b.store.Get(r, "session") + log.Info("Session: %v", session.Values) + if loggedIn, ok := session.Values["logged-in"].(bool); ok && loggedIn { + // They seem to be logged in. Get their user object. + id := session.Values["user-id"].(int) + u, err := users.Load(id) + if err != nil { + log.Error("Error loading user ID %d from session: %v", id, err) + next(w, r) + return + } + + ctx := context.WithValue(r.Context(), userKey, u) + next(w, r.WithContext(ctx)) + } + next(w, r) +} + +// LoginRequired is a middleware that requires a logged-in user. +func (b *Blog) LoginRequired(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { + ctx := r.Context() + if user, ok := ctx.Value(userKey).(*users.User); ok { + if user.ID > 0 { + next(w, r) + } + } + + b.Redirect(w, "/login?next="+r.URL.Path) +} diff --git a/core/models/settings/settings.go b/core/models/settings/settings.go new file mode 100644 index 0000000..e5db21c --- /dev/null +++ b/core/models/settings/settings.go @@ -0,0 +1,84 @@ +package settings + +import ( + "crypto/rand" + "encoding/base64" + + "github.com/kirsle/blog/core/jsondb" +) + +// DB is a reference to the parent app's JsonDB object. +var DB *jsondb.DB + +// Settings holds the global app settings. +type Settings struct { + // Only gets set to true on save(), this determines whether + // the site has ever been configured before. + Initialized bool `json:"initialized"` + + Site struct { + Title string `json:"title"` + AdminEmail string `json:"adminEmail"` + } `json:"site"` + + // Security-related settings. + Security struct { + SecretKey string `json:"secretKey"` // Session cookie secret key + HashCost int `json:"hashCost"` // Bcrypt hash cost for passwords + } `json:"security"` + + // Redis settings for caching in JsonDB. + Redis struct { + Enabled bool `json:"enabled"` + Host string `json:"host"` + Port int `json:"port"` + DB int `json:"db"` + Prefix string `json:"prefix"` + } `json:"redis"` + + // Mail settings + Mail struct{} `json:"mail,omitempty"` +} + +// Defaults returns default settings. The app initially sets this on +// startup before reading your site's saved settings (if available). +// Also this is used as a template when the user first configures their +// site. +func Defaults() *Settings { + s := &Settings{} + s.Site.Title = "Untitled Site" + s.Security.HashCost = 14 + s.Security.SecretKey = RandomKey() + s.Redis.Host = "localhost" + s.Redis.Port = 6379 + s.Redis.DB = 0 + return s +} + +// RandomKey generates a random string to use for the site's secret key. +func RandomKey() string { + keyLength := 32 + + b := make([]byte, keyLength) + rand.Read(b) + return base64.URLEncoding.EncodeToString(b) +} + +// Load the settings. +func Load() (*Settings, error) { + s := &Settings{} + err := DB.Get("app/settings", &s) + return s, err +} + +// Save the site settings. +func (s *Settings) Save() error { + s.Initialized = true + + err := DB.Commit("app/settings", &s) + if err != nil { + return err + } + + return nil +} diff --git a/core/models/users/users.go b/core/models/users/users.go index 2a24a7a..ae67a9b 100644 --- a/core/models/users/users.go +++ b/core/models/users/users.go @@ -122,7 +122,7 @@ func Load(id int) (*User, error) { func (u *User) Save() error { // Sanity check that we have an ID. if u.ID == 0 { - return errors.New("can't save: user does not have an ID!") + return errors.New("can't save a user with no ID") } // Save the main DB file. @@ -175,7 +175,3 @@ func (u *User) key() string { func (u *User) nameKey() string { return "users/by-name/" + u.Username } - -func (u *User) DocumentPath() string { - return "users/by-id/%s" -} diff --git a/core/templates.go b/core/templates.go index 658b602..8829f5c 100644 --- a/core/templates.go +++ b/core/templates.go @@ -3,8 +3,10 @@ package core import ( "html/template" "net/http" + "strings" "github.com/kirsle/blog/core/forms" + "github.com/kirsle/blog/core/models/settings" "github.com/kirsle/blog/core/models/users" ) @@ -13,7 +15,9 @@ import ( // when the template is rendered. type Vars struct { // Global template variables. + SetupNeeded bool Title string + Path string LoggedIn bool CurrentUser *users.User @@ -26,7 +30,17 @@ type Vars struct { // LoadDefaults combines template variables with default, globally available vars. func (v *Vars) LoadDefaults(r *http.Request) { - v.Title = "Untitled Blog" + // Get the site settings. + s, err := settings.Load() + if err != nil { + s = settings.Defaults() + } + + if s.Initialized == false && !strings.HasPrefix(r.URL.Path, "/admin/setup") { + v.SetupNeeded = true + } + v.Title = s.Site.Title + v.Path = r.URL.Path ctx := r.Context() if user, ok := ctx.Value(userKey).(*users.User); ok { diff --git a/root/.layout.gohtml b/root/.layout.gohtml index 24d4778..c21f740 100644 --- a/root/.layout.gohtml +++ b/root/.layout.gohtml @@ -49,11 +49,20 @@
+ {{ if .SetupNeeded }} +
+ Your web blog needs to be set up! + Please click here to + configure your blog. +
+ {{ end }} + {{ if .Flash }}
{{ .Flash }}
{{ end }} + {{ if .Error }}
Error: {{ .Error }}