mirror of
https://git.freecumextremist.com/grumbulon/pomme.git
synced 2024-11-01 00:00:34 +00:00
Merge pull request 'feat: Improved API error handling' (#27) from API-error-handling into master
Reviewed-on: https://git.freecumextremist.com/grumbulon/pomme/pulls/27
This commit is contained in:
commit
419aa53d56
14 changed files with 254 additions and 217 deletions
|
@ -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
|
||||
}
|
||||
|
|
33
docs/docs.go
33
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": {
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,85 +1,16 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"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))
|
||||
})
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
func internalServerError(w http.ResponseWriter, r *http.Request, errMsg string) {
|
||||
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)
|
||||
|
||||
resp := internal.Response{
|
||||
Message: errMsg,
|
||||
HTTPResponse: http.StatusInternalServerError,
|
||||
}
|
||||
|
||||
render.JSON(w, r, resp)
|
||||
|
||||
logger.Error().Msg(errMsg)
|
||||
}
|
||||
|
||||
// API subroute handler.
|
||||
func API() http.Handler {
|
||||
api := chi.NewRouter()
|
||||
|
@ -93,16 +24,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 +46,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)
|
||||
|
|
|
@ -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 {
|
||||
|
@ -170,7 +173,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 +202,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 +311,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 +347,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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,18 +22,19 @@ 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
|
||||
|
||||
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)
|
||||
}
|
||||
|
|
88
internal/api/helpers.go
Normal file
88
internal/api/helpers.go
Normal file
|
@ -0,0 +1,88 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"syscall"
|
||||
"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
|
||||
ok bool
|
||||
)
|
||||
c, err := internal.ReadConfig()
|
||||
if err != nil {
|
||||
logHandler(genericResponseFields{"error": err.Error(), "message": "No config file defined:"})
|
||||
}
|
||||
|
||||
switch r.Header.Get("User-Agent") {
|
||||
case "pomme-api-test-slave":
|
||||
pommeDB, err, ok = db.InitDb(c.TestDB)
|
||||
default:
|
||||
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)
|
||||
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"] {
|
||||
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)
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
@ -16,3 +24,7 @@ type Zone struct {
|
|||
RawFileName string `json:"rawname"`
|
||||
Body string `json:"body,omitempty"`
|
||||
}
|
||||
|
||||
type GenericResponse[T map[string]any] struct {
|
||||
Response map[string]any `json:"response,omitempty"`
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,6 @@ package api
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
|
@ -30,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
|
||||
|
@ -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
|
||||
|
@ -119,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
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
@ -38,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
|
||||
}
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
package util
|
||||
|
||||
// ValidateQuery does nothing.
|
||||
func ValidateQuery(request string) (string, error) {
|
||||
return "", nil
|
||||
}
|
Loading…
Reference in a new issue