From 9d87ae47284340199fce3990339397e46d2be961 Mon Sep 17 00:00:00 2001 From: grumbulon Date: Wed, 1 Feb 2023 15:59:34 -0500 Subject: [PATCH 1/4] 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. From 295360fd05ba60fff3bb91fd82da033ec925f3a8 Mon Sep 17 00:00:00 2001 From: grumbulon Date: Wed, 1 Feb 2023 19:41:38 -0500 Subject: [PATCH 2/4] swagger updates, moved structs and types into types.go, and made helpers.go for custom handlers --- docs/docs.go | 33 +++++++++--------- docs/swagger.json | 33 +++++++++--------- docs/swagger.yaml | 26 +++++++------- internal/api/api.go | 67 ------------------------------------ internal/api/auth.go | 4 +-- internal/api/helpers.go | 75 +++++++++++++++++++++++++++++++++++++++++ internal/api/types.go | 12 ++++++- internal/api/zone.go | 8 ++--- internal/types.go | 6 ++-- 9 files changed, 137 insertions(+), 127 deletions(-) create mode 100644 internal/api/helpers.go diff --git a/docs/docs.go b/docs/docs.go index 0894f37..9c7572d 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -50,13 +50,13 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/internal.GenericResponse-internal_Response" + "$ref": "#/definitions/internal.SwaggerGenericResponse-internal_Response" } }, "401": { "description": "authFailed is a 401 error when logging in fails, includes realm", "schema": { - "$ref": "#/definitions/internal.GenericResponse-internal_Response" + "$ref": "#/definitions/internal.SwaggerGenericResponse-internal_Response" } } } @@ -100,13 +100,13 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/internal.GenericResponse-internal_Response" + "$ref": "#/definitions/internal.SwaggerGenericResponse-internal_Response" } }, "500": { "description": "internalServerError is a 500 server error with a logged error call back", "schema": { - "$ref": "#/definitions/internal.GenericResponse-internal_Response" + "$ref": "#/definitions/internal.SwaggerGenericResponse-internal_Response" } } } @@ -150,13 +150,13 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/internal.GenericResponse-internal_Response" + "$ref": "#/definitions/internal.SwaggerGenericResponse-internal_Response" } }, "500": { "description": "internalServerError is a 500 server error with a logged error call back", "schema": { - "$ref": "#/definitions/internal.GenericResponse-internal_Response" + "$ref": "#/definitions/internal.SwaggerGenericResponse-internal_Response" } } } @@ -164,7 +164,15 @@ const docTemplate = `{ } }, "definitions": { - "internal.GenericResponse-internal_Response": { + "internal.Response": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + } + }, + "internal.SwaggerGenericResponse-internal_Response": { "type": "object", "properties": { "response": { @@ -176,17 +184,6 @@ const docTemplate = `{ ] } } - }, - "internal.Response": { - "type": "object", - "properties": { - "message": { - "type": "string" - }, - "status": { - "type": "integer" - } - } } }, "securityDefinitions": { diff --git a/docs/swagger.json b/docs/swagger.json index 6a98e86..be72e9e 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -41,13 +41,13 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/internal.GenericResponse-internal_Response" + "$ref": "#/definitions/internal.SwaggerGenericResponse-internal_Response" } }, "401": { "description": "authFailed is a 401 error when logging in fails, includes realm", "schema": { - "$ref": "#/definitions/internal.GenericResponse-internal_Response" + "$ref": "#/definitions/internal.SwaggerGenericResponse-internal_Response" } } } @@ -91,13 +91,13 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/internal.GenericResponse-internal_Response" + "$ref": "#/definitions/internal.SwaggerGenericResponse-internal_Response" } }, "500": { "description": "internalServerError is a 500 server error with a logged error call back", "schema": { - "$ref": "#/definitions/internal.GenericResponse-internal_Response" + "$ref": "#/definitions/internal.SwaggerGenericResponse-internal_Response" } } } @@ -141,13 +141,13 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/internal.GenericResponse-internal_Response" + "$ref": "#/definitions/internal.SwaggerGenericResponse-internal_Response" } }, "500": { "description": "internalServerError is a 500 server error with a logged error call back", "schema": { - "$ref": "#/definitions/internal.GenericResponse-internal_Response" + "$ref": "#/definitions/internal.SwaggerGenericResponse-internal_Response" } } } @@ -155,7 +155,15 @@ } }, "definitions": { - "internal.GenericResponse-internal_Response": { + "internal.Response": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + } + }, + "internal.SwaggerGenericResponse-internal_Response": { "type": "object", "properties": { "response": { @@ -167,17 +175,6 @@ ] } } - }, - "internal.Response": { - "type": "object", - "properties": { - "message": { - "type": "string" - }, - "status": { - "type": "integer" - } - } } }, "securityDefinitions": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 537535e..ea7667b 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1,18 +1,16 @@ definitions: - internal.GenericResponse-internal_Response: + internal.Response: + properties: + message: + type: string + type: object + internal.SwaggerGenericResponse-internal_Response: properties: response: allOf: - $ref: '#/definitions/internal.Response' description: Response items type: object - internal.Response: - properties: - message: - type: string - status: - type: integer - type: object info: contact: {} description: Pomme is a service that parses zonefiles @@ -44,11 +42,11 @@ paths: "200": description: OK schema: - $ref: '#/definitions/internal.GenericResponse-internal_Response' + $ref: '#/definitions/internal.SwaggerGenericResponse-internal_Response' "401": description: authFailed is a 401 error when logging in fails, includes realm schema: - $ref: '#/definitions/internal.GenericResponse-internal_Response' + $ref: '#/definitions/internal.SwaggerGenericResponse-internal_Response' summary: authenticate as a regular user tags: - accounts @@ -77,12 +75,12 @@ paths: "200": description: OK schema: - $ref: '#/definitions/internal.GenericResponse-internal_Response' + $ref: '#/definitions/internal.SwaggerGenericResponse-internal_Response' "500": description: internalServerError is a 500 server error with a logged error call back schema: - $ref: '#/definitions/internal.GenericResponse-internal_Response' + $ref: '#/definitions/internal.SwaggerGenericResponse-internal_Response' security: - Bearer: [] summary: parse your zonefile @@ -113,12 +111,12 @@ paths: "200": description: OK schema: - $ref: '#/definitions/internal.GenericResponse-internal_Response' + $ref: '#/definitions/internal.SwaggerGenericResponse-internal_Response' "500": description: internalServerError is a 500 server error with a logged error call back schema: - $ref: '#/definitions/internal.GenericResponse-internal_Response' + $ref: '#/definitions/internal.SwaggerGenericResponse-internal_Response' security: - Bearer: [] summary: upload a zonefile diff --git a/internal/api/api.go b/internal/api/api.go index 8a554f2..e0026ef 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -1,83 +1,16 @@ package api import ( - "context" - "fmt" - "log" "net/http" "time" "git.freecumextremist.com/grumbulon/pomme/internal" - "git.freecumextremist.com/grumbulon/pomme/internal/db" "github.com/go-chi/chi/v5" - "github.com/go-chi/httplog" "github.com/go-chi/httprate" "github.com/go-chi/jwtauth/v5" "github.com/go-chi/render" - "gorm.io/gorm" ) -type key int - -const ( - keyPrincipalContextID key = iota -) - -// setDBMiddleware is the http Handler func for the GORM middleware with context. -func setDBMiddleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - var pommeDB *gorm.DB - c, err := internal.ReadConfig() - if err != nil { - log.Printf("No config file defined: %v", err) - } - - switch r.Header.Get("User-Agent") { - case "pomme-api-test-slave": - pommeDB = db.InitDb(c.TestDB) - default: - pommeDB = db.InitDb(c.DB) - } - - timeoutContext, cancelContext := context.WithTimeout(context.Background(), time.Second) - ctx := context.WithValue(r.Context(), keyPrincipalContextID, pommeDB.WithContext(timeoutContext)) - defer cancelContext() - next.ServeHTTP(w, r.WithContext(ctx)) - }) -} - -type GenericResponse[T map[string]any] struct { - Response map[string]any `json:"response,omitempty"` -} - -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().Set("Content-Type", "application/json; charset=utf-8") - - 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)) - } - - w.WriteHeader(v["status"].(int)) - - 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. func API() http.Handler { api := chi.NewRouter() diff --git a/internal/api/auth.go b/internal/api/auth.go index 137208b..7dbcbd2 100644 --- a/internal/api/auth.go +++ b/internal/api/auth.go @@ -22,8 +22,8 @@ import ( // @Produce json // @Param username query string true "Username" // @Param password query string true "Password" -// @Success 200 {object} internal.GenericResponse[internal.Response] -// @failure 401 {object} internal.GenericResponse[internal.Response] "authFailed is a 401 error when logging in fails, includes realm" +// @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 diff --git a/internal/api/helpers.go b/internal/api/helpers.go new file mode 100644 index 0000000..7d2af27 --- /dev/null +++ b/internal/api/helpers.go @@ -0,0 +1,75 @@ +package api + +import ( + "context" + "fmt" + "log" + "net/http" + "time" + + "git.freecumextremist.com/grumbulon/pomme/internal" + "git.freecumextremist.com/grumbulon/pomme/internal/db" + "github.com/go-chi/httplog" + "github.com/go-chi/render" + "gorm.io/gorm" +) + +// setDBMiddleware is the http Handler func for the GORM middleware with context. +func setDBMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var pommeDB *gorm.DB + c, err := internal.ReadConfig() + if err != nil { + log.Printf("No config file defined: %v", err) + } + + switch r.Header.Get("User-Agent") { + case "pomme-api-test-slave": + pommeDB = db.InitDb(c.TestDB) + default: + pommeDB = db.InitDb(c.DB) + } + + timeoutContext, cancelContext := context.WithTimeout(context.Background(), time.Second) + ctx := context.WithValue(r.Context(), keyPrincipalContextID, pommeDB.WithContext(timeoutContext)) + defer cancelContext() + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +func APIError[T map[string]any](w http.ResponseWriter, r *http.Request, v map[string]any) { + w.Header().Set("X-Content-Type-Options", "nosniff") + w.Header().Set("Content-Type", "application/json; charset=utf-8") + + logHandler(v) + + 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)) + } + + w.WriteHeader(v["status"].(int)) + + delete(v, "error") + + render.JSON(w, r, v) +} + +func logHandler(v map[string]any) { + logger := httplog.NewLogger("Pomme", httplog.Options{ + JSON: true, + }) + + switch v["error"] { + default: + logger.Error().Msg(v["error"].(string)) + fallthrough + case nil: + logger.Info().Msg(v["message"].(string)) + + } + +} diff --git a/internal/api/types.go b/internal/api/types.go index 66ea90d..a56a1cc 100644 --- a/internal/api/types.go +++ b/internal/api/types.go @@ -2,6 +2,14 @@ package api import "gorm.io/gorm" +const ( + keyPrincipalContextID key = iota +) + +type genericResponseFields map[string]any + +type key int + // ZoneRequest represents a Zone file request. type ZoneRequest struct { *Zone @@ -17,4 +25,6 @@ type Zone struct { Body string `json:"body,omitempty"` } -type genericResponseFields map[string]any +type GenericResponse[T map[string]any] struct { + Response map[string]any `json:"response,omitempty"` +} diff --git a/internal/api/zone.go b/internal/api/zone.go index 4e1480c..489d719 100644 --- a/internal/api/zone.go +++ b/internal/api/zone.go @@ -29,8 +29,8 @@ import ( // @Accept mpfd // @Produce json // @Param file formData file true "Zonefile to upload" -// @Success 200 {object} internal.GenericResponse[internal.Response] -// @Failure 500 {object} internal.GenericResponse[internal.Response] "internalServerError is a 500 server error with a logged error call back" +// @Success 200 {object} internal.SwaggerGenericResponse[internal.Response] +// @Failure 500 {object} internal.SwaggerGenericResponse[internal.Response] "internalServerError is a 500 server error with a logged error call back" // @Param Authorization header string true "Bearer Token" // // @Security Bearer @@ -114,8 +114,8 @@ func ReceiveFile(w http.ResponseWriter, r *http.Request) { // @Accept mpfd // @Produce json // @Param filename query string true "Zonefile name" -// @Success 200 {object} internal.GenericResponse[internal.Response] -// @Failure 500 {object} internal.GenericResponse[internal.Response] "internalServerError is a 500 server error with a logged error call back" +// @Success 200 {object} internal.SwaggerGenericResponse[internal.Response] +// @Failure 500 {object} internal.SwaggerGenericResponse[internal.Response] "internalServerError is a 500 server error with a logged error call back" // @Param Authorization header string true "Bearer Token" // // @Security Bearer diff --git a/internal/types.go b/internal/types.go index 6e59d45..5d049e8 100644 --- a/internal/types.go +++ b/internal/types.go @@ -37,9 +37,9 @@ type Config struct { TestDB string } -// GenericNestedResponse[T] -// @Description Some Generic List Response. -type GenericResponse[T any] struct { +// SwaggerGenericResponse[T] +// @Description Generic Response for Swagger. +type SwaggerGenericResponse[T any] struct { // Response items Response T } From 473597682c9ff0c8fadf1704124ea79af1d7035d Mon Sep 17 00:00:00 2001 From: grumbulon Date: Wed, 1 Feb 2023 19:50:41 -0500 Subject: [PATCH 3/4] remove unnecessary code, added switch fallthroughs for cleanup. remove status code from response (is in header) --- internal/api/auth.go | 4 ++-- internal/api/helpers.go | 10 ++++++---- internal/util/util.go | 6 ------ 3 files changed, 8 insertions(+), 12 deletions(-) delete mode 100644 internal/util/util.go diff --git a/internal/api/auth.go b/internal/api/auth.go index 7dbcbd2..434ee2b 100644 --- a/internal/api/auth.go +++ b/internal/api/auth.go @@ -74,7 +74,7 @@ func Login(w http.ResponseWriter, r *http.Request) { db.Where("username = ?", username).First(&result) if result.Username == "" { - APIError(w, r, genericResponseFields{"message": "login failed", "status": http.StatusUnauthorized, "Realm": "authentication"}) + APIError(w, r, genericResponseFields{"message": "login failed", "status": http.StatusUnauthorized, "realm": "authentication"}) return } @@ -82,7 +82,7 @@ func Login(w http.ResponseWriter, r *http.Request) { err = bcrypt.CompareHashAndPassword([]byte(result.HashedPassword), []byte(password)) if err != nil { - APIError(w, r, genericResponseFields{"message": "login failed", "status": http.StatusUnauthorized, "Realm": "authentication"}) + APIError(w, r, genericResponseFields{"message": "login failed", "status": http.StatusUnauthorized, "realm": "authentication"}) return } diff --git a/internal/api/helpers.go b/internal/api/helpers.go index 7d2af27..e31a1bc 100644 --- a/internal/api/helpers.go +++ b/internal/api/helpers.go @@ -44,16 +44,19 @@ func APIError[T map[string]any](w http.ResponseWriter, r *http.Request, v map[st logHandler(v) 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))) + + fallthrough + case nil: w.Header().Add("API Error", v["message"].(string)) } w.WriteHeader(v["status"].(int)) + // remove unnecessary items from response delete(v, "error") + delete(v, "status") render.JSON(w, r, v) } @@ -66,10 +69,9 @@ func logHandler(v map[string]any) { switch v["error"] { default: logger.Error().Msg(v["error"].(string)) + fallthrough case nil: logger.Info().Msg(v["message"].(string)) - } - } diff --git a/internal/util/util.go b/internal/util/util.go deleted file mode 100644 index 393de33..0000000 --- a/internal/util/util.go +++ /dev/null @@ -1,6 +0,0 @@ -package util - -// ValidateQuery does nothing. -func ValidateQuery(request string) (string, error) { - return "", nil -} From 21db5c41660b439dc7246fb32afd14cb71a55264 Mon Sep 17 00:00:00 2001 From: grumbulon Date: Thu, 2 Feb 2023 18:02:22 -0500 Subject: [PATCH 4/4] add server death if cannot acccess DB with clean shutdown (drains connections), and other error handling, linting. --- cmd/pomme/main.go | 26 +++++++++++++++++++++++++- internal/api/api_test.go | 5 ++++- internal/api/helpers.go | 21 ++++++++++++++++----- internal/db/db.go | 14 +++++++++----- 4 files changed, 54 insertions(+), 12 deletions(-) diff --git a/cmd/pomme/main.go b/cmd/pomme/main.go index 14a736d..e8223e0 100644 --- a/cmd/pomme/main.go +++ b/cmd/pomme/main.go @@ -1,8 +1,11 @@ package main import ( + "context" "log" "net/http" + "os" + "os/signal" "time" _ "git.freecumextremist.com/grumbulon/pomme/docs" @@ -49,5 +52,26 @@ func main() { Handler: pomme, } - log.Fatal(s.ListenAndServe()) + idleConnsClosed := make(chan struct{}) + + go func() { + sigint := make(chan os.Signal, 1) + signal.Notify(sigint, os.Interrupt) + <-sigint + + // We received an interrupt signal, shut down. + if err := s.Shutdown(context.Background()); err != nil { + // Error from closing listeners, or context timeout: + log.Printf("HTTP server Shutdown: %v", err) + } + + close(idleConnsClosed) + }() + + if err := s.ListenAndServe(); err != http.ErrServerClosed { // nolint: goerr113 + // Error starting or closing listener: + log.Fatalf("HTTP server ListenAndServe: %v", err) + } + + <-idleConnsClosed } diff --git a/internal/api/api_test.go b/internal/api/api_test.go index d01d25b..e894ea9 100644 --- a/internal/api/api_test.go +++ b/internal/api/api_test.go @@ -78,7 +78,10 @@ func TestAPI(t *testing.T) { assert.NotNil(t, err) } - db := db.InitDb(c.TestDB) + db, err, ok := db.InitDb(c.TestDB) + if err != nil && !ok { + assert.NotNil(t, err) + } hashedPassword, err := bcrypt.GenerateFromPassword([]byte(tester.password), bcrypt.DefaultCost) if err != nil { diff --git a/internal/api/helpers.go b/internal/api/helpers.go index e31a1bc..cd26208 100644 --- a/internal/api/helpers.go +++ b/internal/api/helpers.go @@ -3,8 +3,8 @@ package api import ( "context" "fmt" - "log" "net/http" + "syscall" "time" "git.freecumextremist.com/grumbulon/pomme/internal" @@ -17,17 +17,28 @@ import ( // setDBMiddleware is the http Handler func for the GORM middleware with context. func setDBMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - var pommeDB *gorm.DB + var ( + pommeDB *gorm.DB + ok bool + ) c, err := internal.ReadConfig() if err != nil { - log.Printf("No config file defined: %v", err) + logHandler(genericResponseFields{"error": err.Error(), "message": "No config file defined:"}) } switch r.Header.Get("User-Agent") { case "pomme-api-test-slave": - pommeDB = db.InitDb(c.TestDB) + pommeDB, err, ok = db.InitDb(c.TestDB) default: - pommeDB = db.InitDb(c.DB) + pommeDB, err, ok = db.InitDb(c.DB) + } + + if err != nil && !ok { + logHandler(genericResponseFields{"error": err.Error(), "message": "Error initializing DB"}) + err = syscall.Kill(syscall.Getpid(), syscall.SIGINT) + if err != nil { + panic("idk what to do with this but syscall.Kill errored out.") + } } timeoutContext, cancelContext := context.WithTimeout(context.Background(), time.Second) diff --git a/internal/db/db.go b/internal/db/db.go index 5209b55..0ad431e 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -1,23 +1,27 @@ package db import ( + "fmt" + "git.freecumextremist.com/grumbulon/pomme/internal" "github.com/glebarez/sqlite" "gorm.io/gorm" ) // InitDb is the init function for the database. -func InitDb(path string) *gorm.DB { - db, err := gorm.Open(sqlite.Open(path), &gorm.Config{}) +func InitDb(path string) (db *gorm.DB, err error, ok bool) { + ok = true + db, err = gorm.Open(sqlite.Open(path), &gorm.Config{}) + if err != nil { - panic("failed to connect database") + return db, fmt.Errorf("failed to connect database: %w", err), !ok } // Migrate the schema err = db.AutoMigrate(&internal.User{}, &internal.ZoneRequest{}) if err != nil { - panic("failed to run DB migration") + return &gorm.DB{}, fmt.Errorf("failed to run DB migration: %w", err), !ok } - return db + return db, nil, ok }