From 93ffaa979d840cdbe9496c0a6917096afe2fbd54 Mon Sep 17 00:00:00 2001 From: Gisle Aune Date: Wed, 13 Feb 2019 21:08:03 +0100 Subject: [PATCH 1/3] user-controller: Added /api/user/session endpoint --- controllers/user-controller.go | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/controllers/user-controller.go b/controllers/user-controller.go index 277a0de..2838a99 100644 --- a/controllers/user-controller.go +++ b/controllers/user-controller.go @@ -66,6 +66,30 @@ func (c *UserController) getUser(w http.ResponseWriter, r *http.Request) { respond.Data(w, user) } +func (c *UserController) session(w http.ResponseWriter, r *http.Request) { + type response struct { + LoggedIn bool `json:"loggedIn"` + User *models.User `json:"user"` + } + + session := models.SessionFromContext(r.Context()) + if session == nil { + respond.JSON(w, 200, response{}) + return + } + + user, err := c.users.FindByID(r.Context(), session.UserID) + if err != nil { + respond.JSON(w, 200, response{}) + return + } + + respond.JSON(w, 200, response{ + LoggedIn: true, + User: &user, + }) +} + // login (`POST /login`): Log in as user func (c *UserController) login(w http.ResponseWriter, r *http.Request) { loginData := struct { @@ -197,6 +221,7 @@ func (c *UserController) Mount(router *mux.Router, prefix string) { sub := router.PathPrefix(prefix).Subrouter() sub.HandleFunc("/", c.getUsers).Methods("GET") + sub.HandleFunc("/session", c.session).Methods("GET") sub.HandleFunc("/{id}", c.getUser).Methods("GET") sub.HandleFunc("/login", c.login).Methods("POST") sub.HandleFunc("/logout", c.logout).Methods("POST") From 082db661db0d8b614c6f6ce0a581c678222629e3 Mon Sep 17 00:00:00 2001 From: Gisle Aune Date: Sat, 16 Feb 2019 16:19:04 +0100 Subject: [PATCH 2/3] controllers: Fixed /api/user/session not using respond.Data --- controllers/user-controller.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/controllers/user-controller.go b/controllers/user-controller.go index 2838a99..ff95cde 100644 --- a/controllers/user-controller.go +++ b/controllers/user-controller.go @@ -74,17 +74,17 @@ func (c *UserController) session(w http.ResponseWriter, r *http.Request) { session := models.SessionFromContext(r.Context()) if session == nil { - respond.JSON(w, 200, response{}) + respond.Data(w, response{}) return } user, err := c.users.FindByID(r.Context(), session.UserID) if err != nil { - respond.JSON(w, 200, response{}) + respond.Data(w, response{}) return } - respond.JSON(w, 200, response{ + respond.Data(w, response{ LoggedIn: true, User: &user, }) From 22dddc749e65b26da8084946f7eab131f37851b0 Mon Sep 17 00:00:00 2001 From: Gisle Aune Date: Sat, 23 Feb 2019 14:53:30 +0100 Subject: [PATCH 3/3] lucifer-server, middlewares, httperr: Made session middleware sensible. --- cmd/lucifer-server/main.go | 2 +- go.mod | 1 + go.sum | 2 ++ internal/httperr/error.go | 55 ++++++++++++++++++++++++++++++++++++++ middlewares/session.go | 33 ++++++++++++++++++----- models/user.go | 19 +++++++++++++ 6 files changed, 105 insertions(+), 7 deletions(-) create mode 100644 internal/httperr/error.go diff --git a/cmd/lucifer-server/main.go b/cmd/lucifer-server/main.go index c2da2f5..39c771f 100644 --- a/cmd/lucifer-server/main.go +++ b/cmd/lucifer-server/main.go @@ -47,7 +47,7 @@ func main() { // Router router := mux.NewRouter() - router.Use(middlewares.Session(sqlite.SessionRepository)) + router.Use(middlewares.Session(sqlite.SessionRepository, sqlite.UserRepository)) groupController.Mount(router, "/api/group/") userController.Mount(router, "/api/user/") diff --git a/go.mod b/go.mod index 125f108..6502e0a 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module git.aiterp.net/lucifer/lucifer require ( github.com/collinux/GoHue v0.0.0-20181229002551-d259041d5eb8 // indirect github.com/collinux/gohue v0.0.0-20181229002551-d259041d5eb8 + github.com/google/uuid v1.1.0 github.com/gorilla/context v1.1.1 // indirect github.com/gorilla/mux v1.6.2 github.com/jmoiron/sqlx v1.2.0 diff --git a/go.sum b/go.sum index 8207f6b..ef90c0d 100644 --- a/go.sum +++ b/go.sum @@ -5,6 +5,8 @@ github.com/collinux/gohue v0.0.0-20181229002551-d259041d5eb8/go.mod h1:HFm7vkh/1 github.com/go-sql-driver/mysql v1.4.0 h1:7LxgVwFb2hIQtMm87NdgAVfXjnt4OePseqT1tKx+opk= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/uuid v1.1.0 h1:Jf4mxPC/ziBnoPIdpQdPJ9OeiomAUHLvxmPRSPH9m4s= +github.com/google/uuid v1.1.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/mux v1.6.2 h1:Pgr17XVTNXAk3q/r4CpKzC5xBM/qW1uVLV+IhRZpIIk= diff --git a/internal/httperr/error.go b/internal/httperr/error.go new file mode 100644 index 0000000..fdbc8a8 --- /dev/null +++ b/internal/httperr/error.go @@ -0,0 +1,55 @@ +package httperr + +import ( + "fmt" + "log" + "net/http" + "strconv" + "time" + + "git.aiterp.net/lucifer/lucifer/internal/respond" + "github.com/google/uuid" +) + +// Error is an error that can be used. +type Error struct { + Status int + Kind string + Message string +} + +func (err Error) Error() string { + return err.Kind + ": " + err.Message +} + +// ErrLoginRequired is a common error for when a session is expected, but none is found. +var ErrLoginRequired = Error{Status: 401, Kind: "login_required", Message: "You are not logged in."} + +// NotFound generates a 404 error. +func NotFound(model string) Error { + return Error{Status: 404, Kind: "not_found", Message: model + " not found"} +} + +// Respond responds with the error using the format in the `respond` package. If +// the error is not a httperr.Error, then it'll be logged and a 500 will be returned. +func Respond(w http.ResponseWriter, err error) { + if httpErr, ok := err.(Error); ok { + respond.Error(w, httpErr.Status, httpErr.Kind, httpErr.Message) + } else { + errIDStr := "" + errID, err2 := uuid.NewRandom() + if err2 != nil { + errID, err2 = uuid.NewUUID() + if err2 != nil { + errIDStr = strconv.FormatInt(time.Now().UnixNano(), 36) + } else { + errIDStr = errID.String() + } + } else { + errIDStr = errID.String() + } + + log.Printf("ERROR [%s]: %s", errIDStr, err.Error()) + respond.Error(w, 500, "internal_error", fmt.Sprintf("Something went wrong. It has been logged with the ID %s", errIDStr)) + } +} diff --git a/middlewares/session.go b/middlewares/session.go index 6586805..4c2e743 100644 --- a/middlewares/session.go +++ b/middlewares/session.go @@ -2,15 +2,17 @@ package middlewares import ( "net/http" + "strings" "time" + "git.aiterp.net/lucifer/lucifer/internal/httperr" "git.aiterp.net/lucifer/lucifer/models" "github.com/gorilla/mux" ) // Session is a middleware that adds a Session to the request context if there // is one. -func Session(repo models.SessionRepository) mux.MiddlewareFunc { +func Session(sessions models.SessionRepository, users models.UserRepository) mux.MiddlewareFunc { clearCookie := &http.Cookie{ Name: "lucifer_session", Value: "", @@ -20,32 +22,51 @@ func Session(repo models.SessionRepository) mux.MiddlewareFunc { HttpOnly: true, } + redirectFailure := func(next http.Handler, w http.ResponseWriter, r *http.Request) { + if strings.HasPrefix(r.URL.Path, "/api/user/") { + next.ServeHTTP(w, r) + } else { + httperr.Respond(w, httperr.ErrLoginRequired) + } + } + return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + // Find cookie cookie, err := r.Cookie("lucifer_session") if err != nil || cookie == nil { - next.ServeHTTP(w, r) + redirectFailure(next, w, r) return } // Check session existence - session, err := repo.FindByID(r.Context(), cookie.Value) + session, err := sessions.FindByID(r.Context(), cookie.Value) + if err != nil { + http.SetCookie(w, clearCookie) + redirectFailure(next, w, r) + return + } + ctx = session.InContext(ctx) + + user, err := users.FindByID(r.Context(), session.UserID) if err != nil { http.SetCookie(w, clearCookie) - next.ServeHTTP(w, r) + redirectFailure(next, w, r) return } + ctx = user.InContext(ctx) // Check if session has expired if session.Expired() { http.SetCookie(w, clearCookie) - next.ServeHTTP(w, r) + redirectFailure(next, w, r) return } // Proceed. - next.ServeHTTP(w, r.WithContext(session.InContext(r.Context()))) + next.ServeHTTP(w, r.WithContext(ctx)) }) } } diff --git a/models/user.go b/models/user.go index 6bbd8f2..a89fde4 100644 --- a/models/user.go +++ b/models/user.go @@ -8,6 +8,10 @@ import ( "golang.org/x/crypto/bcrypt" ) +type userCtxKeyType string + +const userCtxKey = userCtxKeyType("luciter_user") + // The User model represents a registered user. type User struct { ID int `db:"id" json:"id"` @@ -42,6 +46,21 @@ func (user *User) CheckPassword(attempt string) error { return nil } +// InContext returns a child context with the value. +func (user *User) InContext(parent context.Context) context.Context { + return context.WithValue(parent, userCtxKey, user) +} + +// UserFromContext gets the user from context, or `nil` if no user is available. +func UserFromContext(ctx context.Context) *User { + v := ctx.Value(userCtxKey) + if v == nil { + return nil + } + + return v.(*User) +} + // UserRepository is an interface for all database operations // the user model makes. type UserRepository interface {