Init site config DB, login required middleware

pull/4/head
Noah 2017-11-07 19:48:22 -08:00
parent fc152d3bde
commit fe84b0c4f1
8 changed files with 205 additions and 36 deletions

View File

@ -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
}

View File

@ -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)

View File

@ -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
}
}

40
core/middleware.go Normal file
View File

@ -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)
}

View File

@ -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
}

View File

@ -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"
}

View File

@ -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 {

View File

@ -49,11 +49,20 @@
<div class="container mb-5">
<div class="row">
<div class="col-9">
{{ if .SetupNeeded }}
<div class="alert alert-success">
Your web blog needs to be set up!
Please <a href="/admin/setup">click here</a> to
configure your blog.
</div>
{{ end }}
{{ if .Flash }}
<div class="alert alert-success">
{{ .Flash }}
</div>
{{ end }}
{{ if .Error }}
<div class="alert alert-danger">
<strong>Error:</strong> {{ .Error }}