diff --git a/core/app.go b/core/app.go index d0bd466..89dac3f 100644 --- a/core/app.go +++ b/core/app.go @@ -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" @@ -22,8 +23,9 @@ type Blog struct { DB *jsondb.DB // Web app objects. - n *negroni.Negroni // Negroni middleware manager - r *mux.Router // Router + 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 diff --git a/core/auth.go b/core/auth.go new file mode 100644 index 0000000..c603d2c --- /dev/null +++ b/core/auth.go @@ -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, "/") +} diff --git a/core/forms/auth.go b/core/forms/auth.go new file mode 100644 index 0000000..007b3f0 --- /dev/null +++ b/core/forms/auth.go @@ -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 +} diff --git a/core/forms/forms.go b/core/forms/forms.go index 0c451b7..a261e60 100644 --- a/core/forms/forms.go +++ b/core/forms/forms.go @@ -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 } diff --git a/core/forms/setup.go b/core/forms/setup.go index 87990b8..d88a7e6 100644 --- a/core/forms/setup.go +++ b/core/forms/setup.go @@ -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 { diff --git a/core/models/users/users.go b/core/models/users/users.go index 8ddd2ce..2a24a7a 100644 --- a/core/models/users/users.go +++ b/core/models/users/users.go @@ -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 { diff --git a/core/templates.go b/core/templates.go index 59b8bd0..658b602 100644 --- a/core/templates.go +++ b/core/templates.go @@ -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 @@ -12,22 +13,33 @@ import ( // when the template is rendered. type Vars struct { // Global template variables. - Title string + 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) diff --git a/root/.layout.gohtml b/root/.layout.gohtml index 515db1f..24d4778 100644 --- a/root/.layout.gohtml +++ b/root/.layout.gohtml @@ -49,6 +49,11 @@
+ {{ if .Flash }} +
+ {{ .Flash }} +
+ {{ end }} {{ if .Error }}
Error: {{ .Error }} @@ -63,6 +68,16 @@

About

+ {{ if .LoggedIn }} + Hello, {{ .CurrentUser.Username }}.
+ Log out
+ {{ if .CurrentUser.Admin }} + Admin center + {{ end }} + {{ else }} + Log in + {{ end }} +

Hello, world!

diff --git a/root/login.gohtml b/root/login.gohtml new file mode 100644 index 0000000..c4ad994 --- /dev/null +++ b/root/login.gohtml @@ -0,0 +1,19 @@ +{{ define "title" }}Sign In{{ end }} +{{ define "content" }} +

Sign In

+ +
+
+
+ +
+
+ +
+
+ + Forgot Password +
+
+
+{{ end }}