From 5fd65da9c057f1b28841fed12409e505ad09044f Mon Sep 17 00:00:00 2001 From: Sung Won Cho Date: Wed, 6 Jan 2021 21:29:06 +1100 Subject: [PATCH] API login/logout --- pkg/server/controllers/helpers.go | 36 ++++++++++++++ pkg/server/controllers/users.go | 80 +++++++++++++++++++++++-------- pkg/server/routes/routes.go | 5 +- 3 files changed, 101 insertions(+), 20 deletions(-) diff --git a/pkg/server/controllers/helpers.go b/pkg/server/controllers/helpers.go index 4f9e4bbd..41b2e3b2 100644 --- a/pkg/server/controllers/helpers.go +++ b/pkg/server/controllers/helpers.go @@ -8,6 +8,7 @@ import ( "time" "github.com/dnote/dnote/pkg/server/app" + "github.com/dnote/dnote/pkg/server/database" "github.com/dnote/dnote/pkg/server/log" "github.com/dnote/dnote/pkg/server/views" "github.com/gorilla/schema" @@ -210,3 +211,38 @@ func handleHTMLError(w http.ResponseWriter, r *http.Request, err error, msg stri v.Render(w, r, d) } + +// handleJSONError logs the error and responds with the given status code with a generic status text +func handleJSONError(w http.ResponseWriter, err error, msg string) { + statusCode := getStatusCode(err) + + rootErr := errors.Cause(err) + + var respText string + if pErr, ok := rootErr.(views.PublicError); ok { + respText = pErr.Public() + } else { + respText = http.StatusText(statusCode) + } + + logError(err, msg) + http.Error(w, respText, statusCode) +} + +// respondWithSession makes a HTTP response with the session from the user with the given userID. +// It sets the HTTP-Only cookie for browser clients and also sends a JSON response for non-browser clients. +func respondWithSession(w http.ResponseWriter, statusCode int, session *database.Session) { + setSessionCookie(w, session.Key, session.ExpiresAt) + + response := SessionResponse{ + Key: session.Key, + ExpiresAt: session.ExpiresAt.Unix(), + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(statusCode) + if err := json.NewEncoder(w).Encode(response); err != nil { + handleJSONError(w, err, "encoding payload") + return + } +} diff --git a/pkg/server/controllers/users.go b/pkg/server/controllers/users.go index 8479df18..53dd6f37 100644 --- a/pkg/server/controllers/users.go +++ b/pkg/server/controllers/users.go @@ -5,8 +5,10 @@ import ( "github.com/dnote/dnote/pkg/server/app" "github.com/dnote/dnote/pkg/server/config" + "github.com/dnote/dnote/pkg/server/database" "github.com/dnote/dnote/pkg/server/log" "github.com/dnote/dnote/pkg/server/views" + "github.com/pkg/errors" ) // NewUsers creates a new Users controller. @@ -75,25 +77,37 @@ type LoginForm struct { Password string `schema:"password" json:"password"` } -// Login handles login -func (u *Users) Login(w http.ResponseWriter, r *http.Request) { - vd := views.Data{} - +func (u *Users) login(r *http.Request) (*database.Session, error) { var form LoginForm if err := parseRequestData(r, &form); err != nil { - handleHTMLError(w, r, err, "parsing request data", u.LoginView, &vd) - return + return nil, err } user, err := u.app.Authenticate(form.Email, form.Password) if err != nil { - handleHTMLError(w, r, err, "authenticating user", u.LoginView, &vd) - return + // If the user is not found, treat it as invalid login + if err == app.ErrNotFound { + return nil, app.ErrLoginInvalid + } + + return nil, err } - session, err := u.app.SignIn(user) + s, err := u.app.SignIn(user) if err != nil { - handleHTMLError(w, r, err, "signing in a user", u.LoginView, &vd) + return nil, err + } + + return s, nil +} + +// Login handles login +func (u *Users) Login(w http.ResponseWriter, r *http.Request) { + vd := views.Data{} + + session, err := u.login(r) + if err != nil { + handleHTMLError(w, r, err, "logging in user", u.LoginView, &vd) return } @@ -101,22 +115,50 @@ func (u *Users) Login(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/", http.StatusFound) } +// V3Login handles login +func (u *Users) V3Login(w http.ResponseWriter, r *http.Request) { + session, err := u.login(r) + if err != nil { + handleJSONError(w, err, "logging in user") + return + } + + respondWithSession(w, http.StatusOK, session) +} + +func (u *Users) logout(r *http.Request) error { + key, err := GetCredential(r) + if err != nil { + return errors.Wrap(err, "getting credentials") + } + + if err = u.app.DeleteSession(key); err != nil { + return errors.Wrap(err, "deleting session") + } + + return nil +} + // Logout handles logout func (u *Users) Logout(w http.ResponseWriter, r *http.Request) { var vd views.Data - key, err := GetCredential(r) - if err != nil { - handleHTMLError(w, r, err, "getting credentials", u.LoginView, &vd) - u.LoginView.Render(w, r, vd) - return - } - - if err = u.app.DeleteSession(key); err != nil { - handleHTMLError(w, r, err, "deleting session", u.LoginView, &vd) + if err := u.logout(r); err != nil { + handleHTMLError(w, r, err, "logging out", u.LoginView, &vd) return } unsetSessionCookie(w) http.Redirect(w, r, "/login", http.StatusFound) } + +// V3Logout handles logout via API +func (u *Users) V3Logout(w http.ResponseWriter, r *http.Request) { + if err := u.logout(r); err != nil { + handleJSONError(w, err, "logging out") + return + } + + unsetSessionCookie(w) + w.WriteHeader(http.StatusNoContent) +} diff --git a/pkg/server/routes/routes.go b/pkg/server/routes/routes.go index a773695a..4061657a 100644 --- a/pkg/server/routes/routes.go +++ b/pkg/server/routes/routes.go @@ -45,7 +45,10 @@ func NewWebRoutes(app *app.App, c *controllers.Controllers) []Route { // NewAPIRoutes returns a new api routes func NewAPIRoutes(c *controllers.Controllers) []Route { - return []Route{} + return []Route{ + {"POST", "/v1/login", Cors(c.Users.V3Login), true}, + {"POST", "/v1/logout", Cors(c.Users.V3Logout), true}, + } } // Config is the configuration for routes