internal.response doesn't includes http status, api tests use header status code now, new generic APIError function

This commit is contained in:
grumbulon 2023-02-01 15:59:34 -05:00
parent 7e149d5996
commit 9d87ae4728
7 changed files with 86 additions and 97 deletions

View file

@ -2,7 +2,6 @@ package api
import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
@ -47,37 +46,36 @@ func setDBMiddleware(next http.Handler) http.Handler {
})
}
// handlers for very common errors.
func authFailed(w http.ResponseWriter, r *http.Request, realm string) {
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Add("WWW-Authenticate", fmt.Sprintf(`Realm="%s"`, realm))
w.WriteHeader(http.StatusUnauthorized)
resp := internal.Response{
Message: fmt.Sprintf(`Login failed -- Realm="%s"`, realm),
HTTPResponse: http.StatusUnauthorized,
}
render.JSON(w, r, resp)
type GenericResponse[T map[string]any] struct {
Response map[string]any `json:"response,omitempty"`
}
func internalServerError(w http.ResponseWriter, r *http.Request, errMsg string) {
func APIError[T map[string]any](w http.ResponseWriter, r *http.Request, v map[string]any) {
logger := httplog.NewLogger("Pomme", httplog.Options{
JSON: true,
})
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Add("Internal Server Error", errMsg)
w.WriteHeader(http.StatusInternalServerError)
w.Header().Set("Content-Type", "application/json; charset=utf-8")
resp := internal.Response{
Message: errMsg,
HTTPResponse: http.StatusInternalServerError,
switch v["realm"] {
case nil:
w.Header().Add("API Error", v["message"].(string))
default:
w.Header().Add("WWW-Authenticate", fmt.Sprintf(`realm="%s"`, v["realm"].(string)))
w.Header().Add("API Error", v["message"].(string))
}
render.JSON(w, r, resp)
w.WriteHeader(v["status"].(int))
logger.Error().Msg(errMsg)
render.JSON(w, r, v)
switch v["error"] {
case nil:
logger.Info().Msg(fmt.Sprint(v["message"]))
default:
logger.Error().Msg(fmt.Sprint(v["error"]))
}
}
// API subroute handler.
@ -93,16 +91,10 @@ func API() http.Handler {
httprate.WithLimitHandler(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusTooManyRequests)
err := json.NewEncoder(w).Encode(
internal.Response{
HTTPResponse: http.StatusTooManyRequests,
Message: "API rate limit exceded",
})
if err != nil {
internalServerError(w, r, "internal server error")
return
resp := internal.Response{
Message: "API rate limit exceded",
}
render.JSON(w, r, resp)
}),
))
api.Use(jwtauth.Verifier(tokenAuth))
@ -121,16 +113,10 @@ func API() http.Handler {
httprate.WithLimitHandler(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusTooManyRequests)
err := json.NewEncoder(w).Encode(
internal.Response{
HTTPResponse: http.StatusTooManyRequests,
Message: "API rate limit exceded",
})
if err != nil {
internalServerError(w, r, "internal server error")
return
resp := internal.Response{
Message: "API rate limit exceded",
}
render.JSON(w, r, resp)
}),
))
api.Use(setDBMiddleware)

View file

@ -170,7 +170,7 @@ func (a *accountTest) TestLogin(t *testing.T) {
assert.NotNil(t, err)
}
assert.Equal(t, http.StatusOK, target.Status)
assert.Equal(t, http.StatusOK, resp.StatusCode)
}
}
@ -199,7 +199,7 @@ func (a *accountTest) TestLogout(t *testing.T) {
assert.NotNil(t, err)
}
assert.Equal(t, http.StatusOK, target.Status)
assert.Equal(t, http.StatusOK, resp.StatusCode)
}
}
@ -308,7 +308,7 @@ func (a *accountTest) TestUpload(t *testing.T) {
assert.NotNil(t, err)
}
assert.Equal(t, tc.expected.response, target.Status)
assert.Equal(t, tc.expected.response, resp.StatusCode)
}
if tc.name == "Should upload a valid file" {
@ -344,6 +344,6 @@ func parse(t *testing.T, fname string, a *accountTest) {
assert.NotNil(t, err)
}
assert.Equal(t, http.StatusOK, target.Status)
assert.Equal(t, http.StatusCreated, resp.StatusCode)
}
}

View file

@ -29,11 +29,12 @@ func Login(w http.ResponseWriter, r *http.Request) {
var result internal.User
if _, err := r.Cookie("jwt"); err == nil {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
resp := internal.Response{
Message: "Already logged in",
HTTPResponse: http.StatusOK,
Message: "Already logged in",
}
render.JSON(w, r, resp)
@ -42,7 +43,7 @@ func Login(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
if err != nil {
internalServerError(w, r, "unable to parse request")
APIError(w, r, genericResponseFields{"message": "internal server error", "status": http.StatusInternalServerError, "error": err.Error()})
return
}
@ -52,20 +53,20 @@ func Login(w http.ResponseWriter, r *http.Request) {
password := r.Form.Get("password")
if username == "" {
internalServerError(w, r, "no username provided") // this should prob be handled by the frontend
APIError(w, r, genericResponseFields{"message": "no password provided", "status": http.StatusInternalServerError})
return
}
if password == "" {
internalServerError(w, r, "no password provided") // this should prob be handled by the frontend
APIError(w, r, genericResponseFields{"message": "no password provided", "status": http.StatusInternalServerError})
return
}
db, ok := r.Context().Value(keyPrincipalContextID).(*gorm.DB)
if !ok {
internalServerError(w, r, "DB connection failed")
APIError(w, r, genericResponseFields{"message": "internal server error", "status": http.StatusInternalServerError, "error": "DB connection failed"})
return
}
@ -73,7 +74,7 @@ func Login(w http.ResponseWriter, r *http.Request) {
db.Where("username = ?", username).First(&result)
if result.Username == "" {
authFailed(w, r, "authentication")
APIError(w, r, genericResponseFields{"message": "login failed", "status": http.StatusUnauthorized, "Realm": "authentication"})
return
}
@ -81,14 +82,14 @@ func Login(w http.ResponseWriter, r *http.Request) {
err = bcrypt.CompareHashAndPassword([]byte(result.HashedPassword), []byte(password))
if err != nil {
authFailed(w, r, "authentication")
APIError(w, r, genericResponseFields{"message": "login failed", "status": http.StatusUnauthorized, "Realm": "authentication"})
return
}
token, err := makeToken(username)
if err != nil {
internalServerError(w, r, err.Error())
APIError(w, r, genericResponseFields{"message": "internal server error", "status": http.StatusInternalServerError, "error": err.Error()})
return
}
@ -104,9 +105,12 @@ func Login(w http.ResponseWriter, r *http.Request) {
Value: token,
})
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
resp := internal.Response{
Message: "Successfully logged in",
HTTPResponse: http.StatusOK,
Message: "Successfully logged in",
}
render.JSON(w, r, resp)
}
@ -122,9 +126,12 @@ func Logout(w http.ResponseWriter, r *http.Request) {
Value: "",
})
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
resp := internal.Response{
Message: "Successfully logged out",
HTTPResponse: http.StatusOK,
Message: "Successfully logged out",
}
render.JSON(w, r, resp)
}

View file

@ -16,3 +16,5 @@ type Zone struct {
RawFileName string `json:"rawname"`
Body string `json:"body,omitempty"`
}
type genericResponseFields map[string]any

View file

@ -2,13 +2,13 @@ package api
import (
"crypto/rand"
"encoding/json"
"fmt"
"math/big"
"net/http"
"time"
"git.freecumextremist.com/grumbulon/pomme/internal"
"github.com/go-chi/render"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
@ -17,14 +17,14 @@ import (
func NewUser(w http.ResponseWriter, r *http.Request) {
db, ok := r.Context().Value(keyPrincipalContextID).(*gorm.DB)
if !ok {
internalServerError(w, r, "internal server error")
APIError(w, r, genericResponseFields{"message": "internal server error", "status": http.StatusInternalServerError, "error": "unable to connect to DB"})
}
var result internal.User
err := r.ParseForm()
if err != nil {
internalServerError(w, r, "unable to parse request")
APIError(w, r, genericResponseFields{"message": "unable to parse request", "status": http.StatusInternalServerError, "error": err.Error()})
return
}
@ -38,7 +38,7 @@ func NewUser(w http.ResponseWriter, r *http.Request) {
password := r.Form.Get("password")
if password == "" {
internalServerError(w, r, "no password provided")
APIError(w, r, genericResponseFields{"message": "no password provided", "status": http.StatusInternalServerError})
return
}
@ -46,14 +46,14 @@ func NewUser(w http.ResponseWriter, r *http.Request) {
db.Where("username = ?", username).First(&result)
if result.Username != "" {
internalServerError(w, r, "user already exists")
APIError(w, r, genericResponseFields{"message": "internal server error", "status": http.StatusInternalServerError})
return
}
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
authFailed(w, r, "login")
APIError(w, r, genericResponseFields{"message": "login failed", "status": http.StatusUnauthorized, "Realm": "authentication"})
return
}
@ -62,7 +62,7 @@ func NewUser(w http.ResponseWriter, r *http.Request) {
token, err := makeToken(username)
if err != nil {
internalServerError(w, r, "internal server error")
APIError(w, r, genericResponseFields{"message": "internal server error", "status": http.StatusInternalServerError, "error": err.Error()})
return
}
@ -78,20 +78,16 @@ func NewUser(w http.ResponseWriter, r *http.Request) {
Value: token,
})
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusCreated)
err = json.NewEncoder(w).Encode(
internal.Response{
HTTPResponse: http.StatusCreated,
Message: "Successfully created account and logged in",
})
if err != nil {
internalServerError(w, r, "internal server error")
return
resp := internal.Response{
Message: "Successfully created account and logged in",
}
render.JSON(w, r, resp)
http.Redirect(w, r, "/", http.StatusSeeOther)
}

View file

@ -2,7 +2,6 @@ package api
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
@ -46,7 +45,7 @@ func ReceiveFile(w http.ResponseWriter, r *http.Request) {
file, header, err := r.FormFile("file")
if err != nil {
internalServerError(w, r, fmt.Sprintf("file upload failed: %v", err))
APIError(w, r, genericResponseFields{"message": "File upload failed", "status": http.StatusInternalServerError, "error": err.Error()})
return
}
@ -63,20 +62,20 @@ func ReceiveFile(w http.ResponseWriter, r *http.Request) {
name := strings.Split(header.Filename, ".")
if _, err = io.Copy(&buf, file); err != nil {
internalServerError(w, r, "internal server error")
APIError(w, r, genericResponseFields{"message": "internal server error", "status": http.StatusInternalServerError, "error": err.Error()})
return
}
if err = util.MakeLocal(name[0], claims["username"].(string), buf); err != nil {
internalServerError(w, r, err.Error())
APIError(w, r, genericResponseFields{"message": "internal server error", "status": http.StatusInternalServerError, "error": err.Error()})
return
}
db, ok := r.Context().Value(keyPrincipalContextID).(*gorm.DB)
if !ok {
internalServerError(w, r, "internal server error")
APIError(w, r, genericResponseFields{"message": "internal server error", "status": http.StatusInternalServerError, "error": "unable to connect to DB"})
return
}
@ -92,19 +91,15 @@ func ReceiveFile(w http.ResponseWriter, r *http.Request) {
buf.Reset()
w.WriteHeader(http.StatusCreated)
w.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(
internal.Response{
HTTPResponse: http.StatusCreated,
Message: "Successfully uploaded zonefile",
})
if err != nil {
internalServerError(w, r, "internal server error")
w.WriteHeader(http.StatusCreated)
return
resp := internal.Response{
Message: "Successfully uploaded zonefile",
}
render.JSON(w, r, resp)
}
// Parse godoc
@ -133,7 +128,7 @@ func ParseZoneFiles(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
if err != nil {
internalServerError(w, r, "unable to parse request")
APIError(w, r, genericResponseFields{"message": "internal server error", "status": http.StatusInternalServerError, "error": err.Error()})
return
}
@ -141,14 +136,14 @@ func ParseZoneFiles(w http.ResponseWriter, r *http.Request) {
filename := r.Form.Get("filename")
if filename == "" {
internalServerError(w, r, "no filename parsed")
APIError(w, r, genericResponseFields{"message": "no filename provided", "status": http.StatusInternalServerError})
return
}
db, ok := r.Context().Value(keyPrincipalContextID).(*gorm.DB)
if !ok {
internalServerError(w, r, "internal server error")
APIError(w, r, genericResponseFields{"message": "internal server error", "status": http.StatusInternalServerError, "error": "unable to connect to DB"})
return
}
@ -161,7 +156,7 @@ func ParseZoneFiles(w http.ResponseWriter, r *http.Request) {
}).First(&result)
if result == (internal.ZoneRequest{}) {
internalServerError(w, r, "internal server error")
APIError(w, r, genericResponseFields{"message": "internal server error", "status": http.StatusInternalServerError})
return
}
@ -169,15 +164,19 @@ func ParseZoneFiles(w http.ResponseWriter, r *http.Request) {
zoneFile := newZoneRequest(result.RawFileName, claims["username"].(string))
if err := zoneFile.Parse(); err != nil {
internalServerError(w, r, fmt.Sprintf("unable to parse zonefile: %v", err))
APIError(w, r, genericResponseFields{"message": "Unable to parse zonefile", "status": http.StatusInternalServerError, "error": err.Error()})
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusCreated)
resp := internal.Response{
Message: "Successfully parsed zonefile",
HTTPResponse: http.StatusOK,
Message: "Successfully parsed zonefile",
}
render.JSON(w, r, resp)
}

View file

@ -11,8 +11,7 @@ type User struct {
// Response struct represents a json response.
type Response struct {
Message string `json:"message,omitempty"`
HTTPResponse int `json:"status,omitempty"`
Message string `json:"message,omitempty"`
}
// ZoneRequest represents a Zone file request.