From 9d87ae47284340199fce3990339397e46d2be961 Mon Sep 17 00:00:00 2001 From: grumbulon Date: Wed, 1 Feb 2023 15:59:34 -0500 Subject: [PATCH] internal.response doesn't includes http status, api tests use header status code now, new generic APIError function --- internal/api/api.go | 64 ++++++++++++++++------------------------ internal/api/api_test.go | 8 ++--- internal/api/auth.go | 35 +++++++++++++--------- internal/api/types.go | 2 ++ internal/api/users.go | 30 ++++++++----------- internal/api/zone.go | 41 +++++++++++++------------ internal/types.go | 3 +- 7 files changed, 86 insertions(+), 97 deletions(-) diff --git a/internal/api/api.go b/internal/api/api.go index 6864c5b..8a554f2 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -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) diff --git a/internal/api/api_test.go b/internal/api/api_test.go index a4126f3..d01d25b 100644 --- a/internal/api/api_test.go +++ b/internal/api/api_test.go @@ -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) } } diff --git a/internal/api/auth.go b/internal/api/auth.go index db03de8..137208b 100644 --- a/internal/api/auth.go +++ b/internal/api/auth.go @@ -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) } diff --git a/internal/api/types.go b/internal/api/types.go index ae07dce..66ea90d 100644 --- a/internal/api/types.go +++ b/internal/api/types.go @@ -16,3 +16,5 @@ type Zone struct { RawFileName string `json:"rawname"` Body string `json:"body,omitempty"` } + +type genericResponseFields map[string]any diff --git a/internal/api/users.go b/internal/api/users.go index 666dc8b..5e831ac 100644 --- a/internal/api/users.go +++ b/internal/api/users.go @@ -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) } diff --git a/internal/api/zone.go b/internal/api/zone.go index 0c039a0..4e1480c 100644 --- a/internal/api/zone.go +++ b/internal/api/zone.go @@ -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) } diff --git a/internal/types.go b/internal/types.go index 0870c3c..6e59d45 100644 --- a/internal/types.go +++ b/internal/types.go @@ -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.