User login and out, sessions and request context

This commit is contained in:
Noah 2017-11-07 09:53:02 -08:00
parent 6f330a3e92
commit fc152d3bde
9 changed files with 188 additions and 18 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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