User login and out, sessions and request context
This commit is contained in:
parent
6f330a3e92
commit
fc152d3bde
|
@ -5,6 +5,7 @@ import (
|
|||
"path/filepath"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/kirsle/blog/core/jsondb"
|
||||
"github.com/kirsle/blog/core/models/users"
|
||||
"github.com/urfave/negroni"
|
||||
|
@ -24,6 +25,7 @@ type Blog struct {
|
|||
// Web app objects.
|
||||
n *negroni.Negroni // Negroni middleware manager
|
||||
r *mux.Router // Router
|
||||
store sessions.Store
|
||||
}
|
||||
|
||||
// New initializes the Blog application.
|
||||
|
@ -32,6 +34,8 @@ 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.
|
||||
|
@ -40,6 +44,8 @@ func New(documentRoot, userRoot string) *Blog {
|
|||
r := mux.NewRouter()
|
||||
blog.r = r
|
||||
r.HandleFunc("/admin/setup", blog.SetupHandler)
|
||||
r.HandleFunc("/login", blog.LoginHandler)
|
||||
r.HandleFunc("/logout", blog.LogoutHandler)
|
||||
r.HandleFunc("/", blog.PageHandler)
|
||||
r.NotFoundHandler = http.HandlerFunc(blog.PageHandler)
|
||||
|
||||
|
@ -48,6 +54,7 @@ func New(documentRoot, userRoot string) *Blog {
|
|||
negroni.NewLogger(),
|
||||
)
|
||||
blog.n = n
|
||||
n.Use(negroni.HandlerFunc(blog.AuthMiddleware))
|
||||
n.UseHandler(r)
|
||||
|
||||
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
|
||||
|
||||
import "net/http"
|
||||
|
||||
// Form is an interface for forms that can validate themselves.
|
||||
type Form interface {
|
||||
Parse(r *http.Request)
|
||||
Validate() error
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@ package forms
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Setup is for the initial blog setup page at /admin/setup.
|
||||
|
@ -12,13 +11,6 @@ type Setup struct {
|
|||
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.
|
||||
func (f Setup) Validate() error {
|
||||
if len(f.Username) == 0 {
|
||||
|
|
|
@ -58,6 +58,25 @@ func Create(u *User) error {
|
|||
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,
|
||||
// u.Password will contain the bcrypt hash.
|
||||
func (u *User) SetPassword(password string) error {
|
||||
|
@ -128,7 +147,7 @@ func nextID() int {
|
|||
|
||||
users, err := DB.List("users/by-id")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
return 1
|
||||
}
|
||||
|
||||
for _, doc := range users {
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"net/http"
|
||||
|
||||
"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
|
||||
|
@ -13,21 +14,32 @@ import (
|
|||
type Vars struct {
|
||||
// Global template variables.
|
||||
Title string
|
||||
LoggedIn bool
|
||||
CurrentUser *users.User
|
||||
|
||||
// Common template variables.
|
||||
Message string
|
||||
Flash string
|
||||
Error error
|
||||
Form forms.Form
|
||||
}
|
||||
|
||||
// LoadDefaults combines template variables with default, globally available vars.
|
||||
func (v *Vars) LoadDefaults() {
|
||||
func (v *Vars) LoadDefaults(r *http.Request) {
|
||||
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.
|
||||
type TemplateVars interface {
|
||||
LoadDefaults()
|
||||
LoadDefaults(*http.Request)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
vars = &Vars{}
|
||||
}
|
||||
vars.LoadDefaults()
|
||||
vars.LoadDefaults(r)
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; encoding=UTF-8")
|
||||
err = t.ExecuteTemplate(w, "layout", vars)
|
||||
|
|
|
@ -49,6 +49,11 @@
|
|||
<div class="container mb-5">
|
||||
<div class="row">
|
||||
<div class="col-9">
|
||||
{{ if .Flash }}
|
||||
<div class="alert alert-success">
|
||||
{{ .Flash }}
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ if .Error }}
|
||||
<div class="alert alert-danger">
|
||||
<strong>Error:</strong> {{ .Error }}
|
||||
|
@ -63,6 +68,16 @@
|
|||
<div class="card-body">
|
||||
<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>
|
||||
</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