Init site config DB, login required middleware
This commit is contained in:
parent
fc152d3bde
commit
fe84b0c4f1
|
@ -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
|
||||
}
|
||||
|
|
27
core/app.go
27
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)
|
||||
|
|
43
core/auth.go
43
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
|
||||
}
|
||||
}
|
||||
|
|
40
core/middleware.go
Normal file
40
core/middleware.go
Normal 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)
|
||||
}
|
84
core/models/settings/settings.go
Normal file
84
core/models/settings/settings.go
Normal 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
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 }}
|
||||
|
|
Loading…
Reference in New Issue
Block a user