2021-04-25 19:33:22 +00:00
|
|
|
package website
|
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
|
|
|
"net/http"
|
|
|
|
|
|
|
|
"git.handmade.network/hmn/hmn/src/auth"
|
|
|
|
"git.handmade.network/hmn/hmn/src/db"
|
|
|
|
"git.handmade.network/hmn/hmn/src/logging"
|
|
|
|
"git.handmade.network/hmn/hmn/src/models"
|
|
|
|
"git.handmade.network/hmn/hmn/src/oops"
|
|
|
|
)
|
|
|
|
|
|
|
|
func Login(c *RequestContext) ResponseData {
|
|
|
|
// TODO: Update this endpoint to give uniform responses on errors and to be resilient to timing attacks.
|
|
|
|
|
|
|
|
form, err := c.GetFormValues()
|
|
|
|
if err != nil {
|
|
|
|
return ErrorResponse(http.StatusBadRequest, NewSafeError(err, "request must contain form data"))
|
|
|
|
}
|
|
|
|
|
|
|
|
username := form.Get("username")
|
|
|
|
password := form.Get("password")
|
|
|
|
if username == "" || password == "" {
|
|
|
|
return ErrorResponse(http.StatusBadRequest, NewSafeError(err, "you must provide both a username and password"))
|
|
|
|
}
|
|
|
|
|
|
|
|
redirect := form.Get("redirect")
|
|
|
|
if redirect == "" {
|
|
|
|
redirect = "/"
|
|
|
|
}
|
|
|
|
|
2021-04-27 03:55:17 +00:00
|
|
|
userRow, err := db.QueryOne(c.Context(), c.Conn, models.User{}, "SELECT $columns FROM auth_user WHERE LOWER(username) = LOWER($1)", username)
|
2021-04-25 19:33:22 +00:00
|
|
|
if err != nil {
|
|
|
|
if errors.Is(err, db.ErrNoMatchingRows) {
|
|
|
|
return ResponseData{
|
|
|
|
StatusCode: http.StatusUnauthorized,
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to look up user by username"))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
user := userRow.(*models.User)
|
|
|
|
|
|
|
|
hashed, err := auth.ParsePasswordString(user.Password)
|
|
|
|
if err != nil {
|
|
|
|
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to parse password string"))
|
|
|
|
}
|
|
|
|
|
|
|
|
passwordsMatch, err := auth.CheckPassword(password, hashed)
|
|
|
|
if err != nil {
|
|
|
|
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to check password against hash"))
|
|
|
|
}
|
|
|
|
|
|
|
|
if passwordsMatch {
|
|
|
|
// re-hash and save the user's password if necessary
|
|
|
|
if hashed.IsOutdated() {
|
|
|
|
newHashed, err := auth.HashPassword(password)
|
|
|
|
if err == nil {
|
2021-04-27 03:55:17 +00:00
|
|
|
err := auth.UpdatePassword(c.Context(), c.Conn, user.Username, newHashed)
|
2021-04-25 19:33:22 +00:00
|
|
|
if err != nil {
|
|
|
|
c.Logger.Error().Err(err).Msg("failed to update user's password")
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
c.Logger.Error().Err(err).Msg("failed to re-hash password")
|
|
|
|
}
|
|
|
|
// If errors happen here, we can still continue with logging them in
|
|
|
|
}
|
|
|
|
|
2021-04-27 03:55:17 +00:00
|
|
|
session, err := auth.CreateSession(c.Context(), c.Conn, user.Username)
|
2021-04-25 19:33:22 +00:00
|
|
|
if err != nil {
|
|
|
|
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to create session"))
|
|
|
|
}
|
|
|
|
|
|
|
|
res := c.Redirect(redirect, http.StatusSeeOther)
|
|
|
|
res.SetCookie(auth.NewSessionCookie(session))
|
|
|
|
|
|
|
|
return res
|
|
|
|
} else {
|
|
|
|
return c.Redirect("/", http.StatusSeeOther) // TODO: Redirect to standalone login page with error
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func Logout(c *RequestContext) ResponseData {
|
|
|
|
sessionCookie, err := c.Req.Cookie(auth.SessionCookieName)
|
|
|
|
if err == nil {
|
|
|
|
// clear the session from the db immediately, no expiration
|
|
|
|
err := auth.DeleteSession(c.Context(), c.Conn, sessionCookie.Value)
|
|
|
|
if err != nil {
|
|
|
|
logging.Error().Err(err).Msg("failed to delete session on logout")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-12 00:48:03 +00:00
|
|
|
redir := c.Req.URL.Query().Get("redirect")
|
|
|
|
if redir == "" {
|
|
|
|
redir = "/"
|
|
|
|
}
|
|
|
|
|
|
|
|
res := c.Redirect(redir, http.StatusSeeOther)
|
2021-04-25 19:33:22 +00:00
|
|
|
res.SetCookie(auth.DeleteSessionCookie)
|
|
|
|
|
|
|
|
return res
|
|
|
|
}
|