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) }