pomme/internal/api/auth.go
2023-05-28 01:36:23 -04:00

181 lines
4.3 KiB
Go

package api
import (
"fmt"
"net/http"
"time"
"dns.froth.zone/pomme/internal"
"github.com/go-chi/render"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
// Auth godoc
//
// @Summary authenticate as a regular user
// @Description login to Pomme
//
// @Description Rate limited: 5 requests every 5 second
//
// @Tags accounts
// @Accept json
// @Produce json
// @Param username query string true "Username"
// @Param password query string true "Password"
// @Success 200 {object} internal.SwaggerGenericResponse[internal.Response]
// @failure 401 {object} internal.SwaggerGenericResponse[internal.Response] "authFailed is a 401 error when logging in fails, includes realm"
// @Router /api/login [post]
func Login(w http.ResponseWriter, r *http.Request) {
var result internal.User
logger, ok := r.Context().Value(keyLoggerContextID).(*Responder)
if !ok {
return
}
if _, err := r.Cookie("jwt"); err == nil {
logger.Response = Response{
Message: "already logged in",
Status: http.StatusOK,
}
logger.newLogEntry().apiError(logger.Response, w, r)
logger.newLogEntry().infoLogger(logger.Response)
return
}
err := r.ParseForm()
if err != nil {
logger.Response = Response{
Message: "unable to parse request",
Status: http.StatusInternalServerError,
Err: err.Error(),
}
logger.newLogEntry().apiError(logger.Response, w, r)
logger.newLogEntry().infoLogger(logger.Response)
return
}
username := r.Form.Get("username")
password := r.Form.Get("password")
if username == "" {
logger.Response = Response{
Message: "no username provided",
Status: http.StatusInternalServerError,
}
logger.newLogEntry().infoLogger(logger.Response)
logger.newLogEntry().apiError(logger.Response, w, r)
return
}
if password == "" {
logger.Response = Response{
Message: "no password provided",
Status: http.StatusInternalServerError,
}
logger.newLogEntry().apiError(logger.Response, w, r)
return
}
db, ok := r.Context().Value(keyPrincipalContextID).(*gorm.DB)
if !ok {
logger.Response = Response{
Message: "no password provided",
Status: http.StatusInternalServerError,
Err: "db connection failed",
}
logger.newLogEntry().apiError(logger.Response, w, r)
logger.newLogEntry().errorLogger(logger.Response)
return
}
db.Where("username = ?", username).First(&result)
if result.Username == "" {
logger.Response = Response{
Message: fmt.Sprintf("login failed: %s", username),
Status: http.StatusUnauthorized,
Realm: "authentication",
}
logger.newLogEntry().apiError(logger.Response, w, r)
logger.newLogEntry().infoLogger(logger.Response)
return
}
err = bcrypt.CompareHashAndPassword([]byte(result.HashedPassword), []byte(password))
if err != nil {
logger.Response = Response{
Message: fmt.Sprintf("login failed: %s", username),
Status: http.StatusUnauthorized,
Realm: "authentication",
}
logger.newLogEntry().apiError(logger.Response, w, r)
logger.newLogEntry().infoLogger(logger.Response)
return
}
token, err := makeToken(username)
if err != nil {
logger.Response = Response{
Message: fmt.Sprintf("login failed: %s", username),
Status: http.StatusUnauthorized,
Realm: "authentication",
}
logger.newLogEntry().apiError(logger.Response, w, r)
logger.newLogEntry().errorLogger(logger.Response)
return
}
http.SetCookie(w, &http.Cookie{
HttpOnly: true,
Expires: time.Now().Add(1 * time.Hour),
MaxAge: 3600,
SameSite: http.SameSiteStrictMode,
// Comment below to disable HTTPS:
Secure: true,
Name: "jwt", // Must be named "jwt" or else the token cannot be searched for by jwtauth.Verifier.
Value: token,
})
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
resp := internal.Response{
Message: "Successfully logged in",
}
render.JSON(w, r, resp)
}
// Logout destroys a users JWT cookie.
func Logout(w http.ResponseWriter, r *http.Request) {
http.SetCookie(w, &http.Cookie{
HttpOnly: true,
MaxAge: -1, // Delete the cookie.
SameSite: http.SameSiteStrictMode,
Secure: true,
Name: "jwt",
Value: "",
})
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
resp := internal.Response{
Message: "Successfully logged out",
}
render.JSON(w, r, resp)
}