Init site config DB, login required middleware
This commit is contained in:
parent
fc152d3bde
commit
fe84b0c4f1
|
@ -3,10 +3,17 @@ package core
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gorilla/sessions"
|
||||||
"github.com/kirsle/blog/core/forms"
|
"github.com/kirsle/blog/core/forms"
|
||||||
|
"github.com/kirsle/blog/core/models/settings"
|
||||||
"github.com/kirsle/blog/core/models/users"
|
"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.
|
// SetupHandler is the initial blog setup route.
|
||||||
func (b *Blog) SetupHandler(w http.ResponseWriter, r *http.Request) {
|
func (b *Blog) SetupHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
vars := &Vars{
|
vars := &Vars{
|
||||||
|
@ -24,6 +31,14 @@ func (b *Blog) SetupHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
vars.Error = err
|
vars.Error = err
|
||||||
} else {
|
} 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)
|
log.Info("Creating admin account %s", form.Username)
|
||||||
user := &users.User{
|
user := &users.User{
|
||||||
Username: form.Username,
|
Username: form.Username,
|
||||||
|
@ -38,6 +53,7 @@ func (b *Blog) SetupHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// All set!
|
// All set!
|
||||||
|
b.Login(w, r, user)
|
||||||
b.Redirect(w, "/admin")
|
b.Redirect(w, "/admin")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
27
core/app.go
27
core/app.go
|
@ -7,6 +7,7 @@ import (
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/gorilla/sessions"
|
"github.com/gorilla/sessions"
|
||||||
"github.com/kirsle/blog/core/jsondb"
|
"github.com/kirsle/blog/core/jsondb"
|
||||||
|
"github.com/kirsle/blog/core/models/settings"
|
||||||
"github.com/kirsle/blog/core/models/users"
|
"github.com/kirsle/blog/core/models/users"
|
||||||
"github.com/urfave/negroni"
|
"github.com/urfave/negroni"
|
||||||
)
|
)
|
||||||
|
@ -34,16 +35,36 @@ func New(documentRoot, userRoot string) *Blog {
|
||||||
DocumentRoot: documentRoot,
|
DocumentRoot: documentRoot,
|
||||||
UserRoot: userRoot,
|
UserRoot: userRoot,
|
||||||
DB: jsondb.New(filepath.Join(userRoot, ".private")),
|
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
|
users.DB = blog.DB
|
||||||
|
|
||||||
|
// Initialize the router.
|
||||||
r := mux.NewRouter()
|
r := mux.NewRouter()
|
||||||
blog.r = r
|
blog.r = r
|
||||||
|
|
||||||
|
// Blog setup.
|
||||||
r.HandleFunc("/admin/setup", blog.SetupHandler)
|
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("/login", blog.LoginHandler)
|
||||||
r.HandleFunc("/logout", blog.LogoutHandler)
|
r.HandleFunc("/logout", blog.LogoutHandler)
|
||||||
r.HandleFunc("/", blog.PageHandler)
|
r.HandleFunc("/", blog.PageHandler)
|
||||||
|
|
41
core/auth.go
41
core/auth.go
|
@ -1,7 +1,6 @@
|
||||||
package core
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
@ -15,24 +14,16 @@ const (
|
||||||
userKey key = iota
|
userKey key = iota
|
||||||
)
|
)
|
||||||
|
|
||||||
// AuthMiddleware loads the user's authentication state.
|
// Login logs the browser in as the given user.
|
||||||
func (b *Blog) AuthMiddleware(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
|
func (b *Blog) Login(w http.ResponseWriter, r *http.Request, u *users.User) error {
|
||||||
session, _ := b.store.Get(r, "session")
|
session, err := b.store.Get(r, "session") // TODO session name
|
||||||
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 {
|
if err != nil {
|
||||||
log.Error("Error loading user ID %d from session: %v", id, err)
|
return err
|
||||||
next(w, r)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
session.Values["logged-in"] = true
|
||||||
ctx := context.WithValue(r.Context(), userKey, u)
|
session.Values["user-id"] = u.ID
|
||||||
next(w, r.WithContext(ctx))
|
session.Save(r, w)
|
||||||
}
|
return nil
|
||||||
next(w, r)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoginHandler shows and handles the login page.
|
// LoginHandler shows and handles the login page.
|
||||||
|
@ -58,18 +49,16 @@ func (b *Blog) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
} else {
|
} else {
|
||||||
// Login OK!
|
// Login OK!
|
||||||
vars.Flash = "Login OK!"
|
vars.Flash = "Login OK!"
|
||||||
|
b.Login(w, r, user)
|
||||||
|
|
||||||
// Log in the user.
|
// A next URL given? TODO: actually get to work
|
||||||
session, err := b.store.Get(r, "session") // TODO session name
|
next := r.FormValue("next")
|
||||||
if err != nil {
|
log.Info("Redirect after login to: %s", next)
|
||||||
vars.Error = err
|
if len(next) > 0 && next[0] == '/' {
|
||||||
|
b.Redirect(w, next)
|
||||||
} else {
|
} else {
|
||||||
session.Values["logged-in"] = true
|
b.Redirect(w, "/")
|
||||||
session.Values["user-id"] = user.ID
|
|
||||||
session.Save(r, w)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
b.Redirect(w, "/login")
|
|
||||||
return
|
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 {
|
func (u *User) Save() error {
|
||||||
// Sanity check that we have an ID.
|
// Sanity check that we have an ID.
|
||||||
if u.ID == 0 {
|
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.
|
// Save the main DB file.
|
||||||
|
@ -175,7 +175,3 @@ func (u *User) key() string {
|
||||||
func (u *User) nameKey() string {
|
func (u *User) nameKey() string {
|
||||||
return "users/by-name/" + u.Username
|
return "users/by-name/" + u.Username
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *User) DocumentPath() string {
|
|
||||||
return "users/by-id/%s"
|
|
||||||
}
|
|
||||||
|
|
|
@ -3,8 +3,10 @@ package core
|
||||||
import (
|
import (
|
||||||
"html/template"
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/kirsle/blog/core/forms"
|
"github.com/kirsle/blog/core/forms"
|
||||||
|
"github.com/kirsle/blog/core/models/settings"
|
||||||
"github.com/kirsle/blog/core/models/users"
|
"github.com/kirsle/blog/core/models/users"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -13,7 +15,9 @@ import (
|
||||||
// when the template is rendered.
|
// when the template is rendered.
|
||||||
type Vars struct {
|
type Vars struct {
|
||||||
// Global template variables.
|
// Global template variables.
|
||||||
|
SetupNeeded bool
|
||||||
Title string
|
Title string
|
||||||
|
Path string
|
||||||
LoggedIn bool
|
LoggedIn bool
|
||||||
CurrentUser *users.User
|
CurrentUser *users.User
|
||||||
|
|
||||||
|
@ -26,7 +30,17 @@ type Vars struct {
|
||||||
|
|
||||||
// LoadDefaults combines template variables with default, globally available vars.
|
// LoadDefaults combines template variables with default, globally available vars.
|
||||||
func (v *Vars) LoadDefaults(r *http.Request) {
|
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()
|
ctx := r.Context()
|
||||||
if user, ok := ctx.Value(userKey).(*users.User); ok {
|
if user, ok := ctx.Value(userKey).(*users.User); ok {
|
||||||
|
|
|
@ -49,11 +49,20 @@
|
||||||
<div class="container mb-5">
|
<div class="container mb-5">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-9">
|
<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 }}
|
{{ if .Flash }}
|
||||||
<div class="alert alert-success">
|
<div class="alert alert-success">
|
||||||
{{ .Flash }}
|
{{ .Flash }}
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
{{ if .Error }}
|
{{ if .Error }}
|
||||||
<div class="alert alert-danger">
|
<div class="alert alert-danger">
|
||||||
<strong>Error:</strong> {{ .Error }}
|
<strong>Error:</strong> {{ .Error }}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user