User login and out, sessions and request context
This commit is contained in:
parent
6f330a3e92
commit
fc152d3bde
11
core/app.go
11
core/app.go
|
@ -5,6 +5,7 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/gorilla/sessions"
|
||||||
"github.com/kirsle/blog/core/jsondb"
|
"github.com/kirsle/blog/core/jsondb"
|
||||||
"github.com/kirsle/blog/core/models/users"
|
"github.com/kirsle/blog/core/models/users"
|
||||||
"github.com/urfave/negroni"
|
"github.com/urfave/negroni"
|
||||||
|
@ -22,8 +23,9 @@ type Blog struct {
|
||||||
DB *jsondb.DB
|
DB *jsondb.DB
|
||||||
|
|
||||||
// Web app objects.
|
// Web app objects.
|
||||||
n *negroni.Negroni // Negroni middleware manager
|
n *negroni.Negroni // Negroni middleware manager
|
||||||
r *mux.Router // Router
|
r *mux.Router // Router
|
||||||
|
store sessions.Store
|
||||||
}
|
}
|
||||||
|
|
||||||
// New initializes the Blog application.
|
// New initializes the Blog application.
|
||||||
|
@ -32,6 +34,8 @@ 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.
|
// Initialize all the models.
|
||||||
|
@ -40,6 +44,8 @@ func New(documentRoot, userRoot string) *Blog {
|
||||||
r := mux.NewRouter()
|
r := mux.NewRouter()
|
||||||
blog.r = r
|
blog.r = r
|
||||||
r.HandleFunc("/admin/setup", blog.SetupHandler)
|
r.HandleFunc("/admin/setup", blog.SetupHandler)
|
||||||
|
r.HandleFunc("/login", blog.LoginHandler)
|
||||||
|
r.HandleFunc("/logout", blog.LogoutHandler)
|
||||||
r.HandleFunc("/", blog.PageHandler)
|
r.HandleFunc("/", blog.PageHandler)
|
||||||
r.NotFoundHandler = http.HandlerFunc(blog.PageHandler)
|
r.NotFoundHandler = http.HandlerFunc(blog.PageHandler)
|
||||||
|
|
||||||
|
@ -48,6 +54,7 @@ func New(documentRoot, userRoot string) *Blog {
|
||||||
negroni.NewLogger(),
|
negroni.NewLogger(),
|
||||||
)
|
)
|
||||||
blog.n = n
|
blog.n = n
|
||||||
|
n.Use(negroni.HandlerFunc(blog.AuthMiddleware))
|
||||||
n.UseHandler(r)
|
n.UseHandler(r)
|
||||||
|
|
||||||
return blog
|
return blog
|
||||||
|
|
88
core/auth.go
Normal file
88
core/auth.go
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/kirsle/blog/core/forms"
|
||||||
|
"github.com/kirsle/blog/core/models/users"
|
||||||
|
)
|
||||||
|
|
||||||
|
type key int
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
next(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoginHandler shows and handles the login page.
|
||||||
|
func (b *Blog) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := &Vars{
|
||||||
|
Form: forms.Setup{},
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.Method == "POST" {
|
||||||
|
form := &forms.Login{
|
||||||
|
Username: r.FormValue("username"),
|
||||||
|
Password: r.FormValue("password"),
|
||||||
|
}
|
||||||
|
vars.Form = form
|
||||||
|
err := form.Validate()
|
||||||
|
if err != nil {
|
||||||
|
vars.Error = err
|
||||||
|
} else {
|
||||||
|
// Test the login.
|
||||||
|
user, err := users.CheckAuth(form.Username, form.Password)
|
||||||
|
if err != nil {
|
||||||
|
vars.Error = errors.New("bad username or password")
|
||||||
|
} else {
|
||||||
|
// Login OK!
|
||||||
|
vars.Flash = "Login OK!"
|
||||||
|
|
||||||
|
// Log in the user.
|
||||||
|
session, err := b.store.Get(r, "session") // TODO session name
|
||||||
|
if err != nil {
|
||||||
|
vars.Error = err
|
||||||
|
} else {
|
||||||
|
session.Values["logged-in"] = true
|
||||||
|
session.Values["user-id"] = user.ID
|
||||||
|
session.Save(r, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.Redirect(w, "/login")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
b.RenderTemplate(w, r, "login", vars)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogoutHandler logs the user out and redirects to the home page.
|
||||||
|
func (b *Blog) LogoutHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
session, _ := b.store.Get(r, "session")
|
||||||
|
delete(session.Values, "logged-in")
|
||||||
|
delete(session.Values, "user-id")
|
||||||
|
session.Save(r, w)
|
||||||
|
b.Redirect(w, "/")
|
||||||
|
}
|
21
core/forms/auth.go
Normal file
21
core/forms/auth.go
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
package forms
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Login is for signing into an account.
|
||||||
|
type Login struct {
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the form.
|
||||||
|
func (f Login) Validate() error {
|
||||||
|
if len(f.Username) == 0 {
|
||||||
|
return errors.New("username is required")
|
||||||
|
} else if len(f.Password) == 0 {
|
||||||
|
return errors.New("password is required")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -1,9 +1,6 @@
|
||||||
package forms
|
package forms
|
||||||
|
|
||||||
import "net/http"
|
|
||||||
|
|
||||||
// Form is an interface for forms that can validate themselves.
|
// Form is an interface for forms that can validate themselves.
|
||||||
type Form interface {
|
type Form interface {
|
||||||
Parse(r *http.Request)
|
|
||||||
Validate() error
|
Validate() error
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,6 @@ package forms
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Setup is for the initial blog setup page at /admin/setup.
|
// Setup is for the initial blog setup page at /admin/setup.
|
||||||
|
@ -12,13 +11,6 @@ type Setup struct {
|
||||||
Confirm string
|
Confirm string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse the form.
|
|
||||||
func (f Setup) Parse(r *http.Request) {
|
|
||||||
f.Username = r.FormValue("username")
|
|
||||||
f.Password = r.FormValue("password")
|
|
||||||
f.Confirm = r.FormValue("confirm")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate the form.
|
// Validate the form.
|
||||||
func (f Setup) Validate() error {
|
func (f Setup) Validate() error {
|
||||||
if len(f.Username) == 0 {
|
if len(f.Username) == 0 {
|
||||||
|
|
|
@ -58,6 +58,25 @@ func Create(u *User) error {
|
||||||
return u.Save()
|
return u.Save()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CheckAuth tests a login with a username and password.
|
||||||
|
func CheckAuth(username, password string) (*User, error) {
|
||||||
|
username = Normalize(username)
|
||||||
|
|
||||||
|
// Look up the user by username.
|
||||||
|
u, err := LoadUsername(username)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the password.
|
||||||
|
err = bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return u, nil
|
||||||
|
}
|
||||||
|
|
||||||
// SetPassword sets a user's password by bcrypt hashing it. After this function,
|
// SetPassword sets a user's password by bcrypt hashing it. After this function,
|
||||||
// u.Password will contain the bcrypt hash.
|
// u.Password will contain the bcrypt hash.
|
||||||
func (u *User) SetPassword(password string) error {
|
func (u *User) SetPassword(password string) error {
|
||||||
|
@ -128,7 +147,7 @@ func nextID() int {
|
||||||
|
|
||||||
users, err := DB.List("users/by-id")
|
users, err := DB.List("users/by-id")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, doc := range users {
|
for _, doc := range users {
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/kirsle/blog/core/forms"
|
"github.com/kirsle/blog/core/forms"
|
||||||
|
"github.com/kirsle/blog/core/models/users"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Vars is an interface to implement by the templates to pass their own custom
|
// Vars is an interface to implement by the templates to pass their own custom
|
||||||
|
@ -12,22 +13,33 @@ import (
|
||||||
// when the template is rendered.
|
// when the template is rendered.
|
||||||
type Vars struct {
|
type Vars struct {
|
||||||
// Global template variables.
|
// Global template variables.
|
||||||
Title string
|
Title string
|
||||||
|
LoggedIn bool
|
||||||
|
CurrentUser *users.User
|
||||||
|
|
||||||
// Common template variables.
|
// Common template variables.
|
||||||
Message string
|
Message string
|
||||||
|
Flash string
|
||||||
Error error
|
Error error
|
||||||
Form forms.Form
|
Form forms.Form
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadDefaults combines template variables with default, globally available vars.
|
// LoadDefaults combines template variables with default, globally available vars.
|
||||||
func (v *Vars) LoadDefaults() {
|
func (v *Vars) LoadDefaults(r *http.Request) {
|
||||||
v.Title = "Untitled Blog"
|
v.Title = "Untitled Blog"
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
if user, ok := ctx.Value(userKey).(*users.User); ok {
|
||||||
|
if user.ID > 0 {
|
||||||
|
v.LoggedIn = true
|
||||||
|
v.CurrentUser = user
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TemplateVars is an interface that describes the template variable struct.
|
// TemplateVars is an interface that describes the template variable struct.
|
||||||
type TemplateVars interface {
|
type TemplateVars interface {
|
||||||
LoadDefaults()
|
LoadDefaults(*http.Request)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RenderTemplate responds with an HTML template.
|
// RenderTemplate responds with an HTML template.
|
||||||
|
@ -58,7 +70,7 @@ func (b *Blog) RenderTemplate(w http.ResponseWriter, r *http.Request, path strin
|
||||||
if vars == nil {
|
if vars == nil {
|
||||||
vars = &Vars{}
|
vars = &Vars{}
|
||||||
}
|
}
|
||||||
vars.LoadDefaults()
|
vars.LoadDefaults(r)
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "text/html; encoding=UTF-8")
|
w.Header().Set("Content-Type", "text/html; encoding=UTF-8")
|
||||||
err = t.ExecuteTemplate(w, "layout", vars)
|
err = t.ExecuteTemplate(w, "layout", vars)
|
||||||
|
|
|
@ -49,6 +49,11 @@
|
||||||
<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 .Flash }}
|
||||||
|
<div class="alert alert-success">
|
||||||
|
{{ .Flash }}
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
{{ if .Error }}
|
{{ if .Error }}
|
||||||
<div class="alert alert-danger">
|
<div class="alert alert-danger">
|
||||||
<strong>Error:</strong> {{ .Error }}
|
<strong>Error:</strong> {{ .Error }}
|
||||||
|
@ -63,6 +68,16 @@
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h4 class="card-title">About</h4>
|
<h4 class="card-title">About</h4>
|
||||||
|
|
||||||
|
{{ if .LoggedIn }}
|
||||||
|
Hello, {{ .CurrentUser.Username }}.<br>
|
||||||
|
<a href="/logout">Log out</a><br>
|
||||||
|
{{ if .CurrentUser.Admin }}
|
||||||
|
<a href="/admin">Admin center</a>
|
||||||
|
{{ end }}
|
||||||
|
{{ else }}
|
||||||
|
<a href="/login">Log in</a>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
<p>Hello, world!</p>
|
<p>Hello, world!</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
19
root/login.gohtml
Normal file
19
root/login.gohtml
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
{{ define "title" }}Sign In{{ end }}
|
||||||
|
{{ define "content" }}
|
||||||
|
<h1>Sign In</h1>
|
||||||
|
|
||||||
|
<form name="login" action="/login" method="POST">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<input type="text" name="username" class="form-control" placeholder="Username">
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<input type="password" name="password" class="form-control" placeholder="Password">
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<button type="submit" class="btn btn-primary">Sign In</button>
|
||||||
|
<a href="/" class="btn btn-secondary">Forgot Password</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{{ end }}
|
Loading…
Reference in New Issue
Block a user