Mercurial > gemma
changeset 4244:4394daeea96a json-handler-middleware
Moved JSONHandler into middleware package.
author | Sascha L. Teichmann <sascha.teichmann@intevation.de> |
---|---|
date | Thu, 22 Aug 2019 11:26:48 +0200 |
parents | d776110b4db0 |
children | df198705300e |
files | pkg/controllers/cross.go pkg/controllers/diff.go pkg/controllers/gauges.go pkg/controllers/importconfig.go pkg/controllers/importqueue.go pkg/controllers/json.go pkg/controllers/manualimports.go pkg/controllers/printtemplates.go pkg/controllers/publish.go pkg/controllers/pwreset.go pkg/controllers/routes.go pkg/controllers/search.go pkg/controllers/srimports.go pkg/controllers/surveys.go pkg/controllers/system.go pkg/controllers/token.go pkg/controllers/uploadedimports.go pkg/controllers/user.go pkg/middleware/json.go pkg/middleware/jsonhandler.go |
diffstat | 20 files changed, 460 insertions(+), 430 deletions(-) [+] |
line wrap: on
line diff
--- a/pkg/controllers/cross.go Thu Aug 22 10:54:08 2019 +0200 +++ b/pkg/controllers/cross.go Thu Aug 22 11:26:48 2019 +0200 @@ -23,6 +23,8 @@ "gemma.intevation.de/gemma/pkg/models" "gemma.intevation.de/gemma/pkg/octree" + + mw "gemma.intevation.de/gemma/pkg/middleware" ) func reproject( @@ -64,13 +66,13 @@ return mls, err } -func crossSection(req *http.Request) (jr JSONResult, err error) { +func crossSection(req *http.Request) (jr mw.JSONResult, err error) { - csi := JSONInput(req).(*models.CrossSectionInput) + csi := mw.JSONInput(req).(*models.CrossSectionInput) start := time.Now() ctx := req.Context() - conn := JSONConn(req) + conn := mw.JSONConn(req) tree, err := octree.FromCache( ctx, conn, @@ -82,7 +84,7 @@ } if tree == nil { - err = JSONError{ + err = mw.JSONError{ Code: http.StatusNotFound, Message: fmt.Sprintf("Cannot find survey for %s/%s.", csi.Properties.Bottleneck, @@ -156,7 +158,7 @@ return } - jr = JSONResult{ + jr = mw.JSONResult{ Result: &models.CrossSectionOutput{ Type: "Feature", Geometry: models.CrossSectionOutputGeometry{
--- a/pkg/controllers/diff.go Thu Aug 22 10:54:08 2019 +0200 +++ b/pkg/controllers/diff.go Thu Aug 22 11:26:48 2019 +0200 @@ -26,6 +26,8 @@ "gemma.intevation.de/gemma/pkg/common" "gemma.intevation.de/gemma/pkg/models" "gemma.intevation.de/gemma/pkg/octree" + + mw "gemma.intevation.de/gemma/pkg/middleware" ) const ( @@ -84,16 +86,16 @@ // TODO: Make this configurable? var diffCalculationSemaphore = semaphore.NewWeighted(int64(3)) -func diffCalculation(req *http.Request) (jr JSONResult, err error) { +func diffCalculation(req *http.Request) (jr mw.JSONResult, err error) { begin := time.Now() start := begin - dci := JSONInput(req).(*models.DiffCalculationInput) + dci := mw.JSONInput(req).(*models.DiffCalculationInput) ctx := req.Context() - conn := JSONConn(req) + conn := mw.JSONConn(req) var id int64 err = conn.QueryRowContext( @@ -111,7 +113,7 @@ return default: // We already have this diff - jr = JSONResult{ + jr = mw.JSONResult{ Result: map[string]int64{"id": id}, } return @@ -133,7 +135,7 @@ } if minuendTree == nil { - err = JSONError{ + err = mw.JSONError{ Code: http.StatusNotFound, Message: fmt.Sprintf("Cannot find survey for %s/%s.", dci.Bottleneck, @@ -154,7 +156,7 @@ } if subtrahendTree == nil { - err = JSONError{ + err = mw.JSONError{ Code: http.StatusNotFound, Message: fmt.Sprintf("Cannot find survey for %s/%s.", dci.Bottleneck, @@ -165,7 +167,7 @@ // We need a slow path implementation for this. if minuendTree.EPSG != subtrahendTree.EPSG { - err = JSONError{ + err = mw.JSONError{ Code: http.StatusInternalServerError, Message: "Calculating differences between two different " + "EPSG code octrees are not supported, yet.", @@ -315,7 +317,7 @@ log.Printf("info: difference calculation succeed after %v\n", time.Since(begin)) - jr = JSONResult{ + jr = mw.JSONResult{ Result: map[string]int64{"id": id}, } return
--- a/pkg/controllers/gauges.go Thu Aug 22 10:54:08 2019 +0200 +++ b/pkg/controllers/gauges.go Thu Aug 22 11:26:48 2019 +0200 @@ -31,8 +31,9 @@ "gonum.org/v1/gonum/stat" "gemma.intevation.de/gemma/pkg/common" - "gemma.intevation.de/gemma/pkg/middleware" "gemma.intevation.de/gemma/pkg/models" + + mw "gemma.intevation.de/gemma/pkg/middleware" ) const ( @@ -192,7 +193,7 @@ year, _ := strconv.Atoi(mux.Vars(req)["year"]) - conn := middleware.GetDBConn(req) + conn := mw.GetDBConn(req) ctx := req.Context() @@ -282,7 +283,7 @@ return } - conn := middleware.GetDBConn(req) + conn := mw.GetDBConn(req) ctx := req.Context() @@ -446,7 +447,7 @@ func parseISRS(code string) (*models.Isrs, error) { isrs, err := models.IsrsFromString(code) if err != nil { - return nil, JSONError{ + return nil, mw.JSONError{ Code: http.StatusBadRequest, Message: fmt.Sprintf("error: Invalid ISRS code: %v", err), } @@ -574,7 +575,7 @@ return values, nil } -func nashSutcliffe(req *http.Request) (jr JSONResult, err error) { +func nashSutcliffe(req *http.Request) (jr mw.JSONResult, err error) { gauge := mux.Vars(req)["gauge"] @@ -586,7 +587,7 @@ var when time.Time if w := req.FormValue("when"); w != "" { if when, err = common.ParseTime(w); err != nil { - err = JSONError{ + err = mw.JSONError{ Code: http.StatusBadRequest, Message: fmt.Sprintf("error: wrong time format: %v", err), } @@ -601,7 +602,7 @@ var values []observedPredictedValues - if values, err = loadNashSutcliffeData(ctx, JSONConn(req), isrs, when); err != nil { + if values, err = loadNashSutcliffeData(ctx, mw.JSONConn(req), isrs, when); err != nil { return } @@ -640,7 +641,7 @@ observed = observed[:0] } - jr = JSONResult{ + jr = mw.JSONResult{ Result: &coeffs{ When: models.ImportTime{Time: when}, Coeffs: cs, @@ -726,7 +727,7 @@ stmt.WriteString(selectWaterlevelsSQL) filters.serialize(&stmt, &args) - conn := middleware.GetDBConn(req) + conn := mw.GetDBConn(req) ctx := req.Context()
--- a/pkg/controllers/importconfig.go Thu Aug 22 10:54:08 2019 +0200 +++ b/pkg/controllers/importconfig.go Thu Aug 22 11:26:48 2019 +0200 @@ -26,16 +26,18 @@ "gemma.intevation.de/gemma/pkg/common" "gemma.intevation.de/gemma/pkg/imports" "gemma.intevation.de/gemma/pkg/scheduler" + + mw "gemma.intevation.de/gemma/pkg/middleware" ) -func runImportConfig(req *http.Request) (jr JSONResult, err error) { +func runImportConfig(req *http.Request) (jr mw.JSONResult, err error) { id, _ := strconv.ParseInt(mux.Vars(req)["id"], 10, 64) ctx := req.Context() var jobID int64 - if jobID, err = imports.RunConfiguredImportContext(ctx, JSONConn(req), id); err != nil { + if jobID, err = imports.RunConfiguredImportContext(ctx, mw.JSONConn(req), id); err != nil { return } @@ -45,28 +47,28 @@ ID: jobID, } - jr = JSONResult{ + jr = mw.JSONResult{ Code: http.StatusCreated, Result: &result, } return } -func modifyImportConfig(req *http.Request) (jr JSONResult, err error) { +func modifyImportConfig(req *http.Request) (jr mw.JSONResult, err error) { ctx := req.Context() - raw := JSONInput(req).(*json.RawMessage) + raw := mw.JSONInput(req).(*json.RawMessage) id, _ := strconv.ParseInt(mux.Vars(req)["id"], 10, 64) - conn := JSONConn(req) + conn := mw.JSONConn(req) var pc *imports.PersistentConfig pc, err = imports.LoadPersistentConfigContext(ctx, conn, id) switch { case err == sql.ErrNoRows: - err = JSONError{ + err = mw.JSONError{ Code: http.StatusNotFound, Message: fmt.Sprintf("No configuration %d found", id), } @@ -78,7 +80,7 @@ kind := imports.JobKind(pc.Kind) ctor := imports.ImportModelForJobKind(kind) if ctor == nil { - err = JSONError{ + err = mw.JSONError{ Code: http.StatusInternalServerError, Message: fmt.Sprintf("No constructor for kind '%s' found", pc.Kind), } @@ -139,11 +141,11 @@ ID: id, } - jr = JSONResult{Result: &result} + jr = mw.JSONResult{Result: &result} return } -func infoImportConfig(req *http.Request) (jr JSONResult, err error) { +func infoImportConfig(req *http.Request) (jr mw.JSONResult, err error) { ctx := req.Context() @@ -151,12 +153,12 @@ var cfg *imports.PersistentConfig - cfg, err = imports.LoadPersistentConfigContext(ctx, JSONConn(req), id) + cfg, err = imports.LoadPersistentConfigContext(ctx, mw.JSONConn(req), id) switch { case err != nil: return case cfg == nil: - err = JSONError{ + err = mw.JSONError{ Code: http.StatusNotFound, Message: fmt.Sprintf("No schedule %d found", id), } @@ -167,7 +169,7 @@ ctor := imports.ImportModelForJobKind(kind) if ctor == nil { - err = JSONError{ + err = mw.JSONError{ Code: http.StatusInternalServerError, Message: fmt.Sprintf("No constructor for kind '%s' found", cfg.Kind), } @@ -190,7 +192,7 @@ return } - jr = JSONResult{Result: &imports.ImportConfigOut{ + jr = mw.JSONResult{Result: &imports.ImportConfigOut{ ID: id, Kind: imports.ImportKind(cfg.Kind), Config: what, @@ -198,14 +200,14 @@ return } -func deleteImportConfig(req *http.Request) (jr JSONResult, err error) { +func deleteImportConfig(req *http.Request) (jr mw.JSONResult, err error) { ctx := req.Context() id, _ := strconv.ParseInt(mux.Vars(req)["id"], 10, 64) var tx *sql.Tx - if tx, err = JSONConn(req).BeginTx(ctx, nil); err != nil { + if tx, err = mw.JSONConn(req).BeginTx(ctx, nil); err != nil { return } defer tx.Rollback() @@ -218,7 +220,7 @@ switch { case err == sql.ErrNoRows: - err = JSONError{ + err = mw.JSONError{ Code: http.StatusNotFound, Message: fmt.Sprintf("No configuration %d found", id), } @@ -240,20 +242,20 @@ ID: id, } - jr = JSONResult{Result: &result} + jr = mw.JSONResult{Result: &result} return } -func addImportConfig(req *http.Request) (jr JSONResult, err error) { +func addImportConfig(req *http.Request) (jr mw.JSONResult, err error) { - cfg := JSONInput(req).(*imports.ImportConfigIn) + cfg := mw.JSONInput(req).(*imports.ImportConfigIn) kind := imports.JobKind(cfg.Kind) ctor := imports.ImportModelForJobKind(kind) if ctor == nil { - err = JSONError{ + err = mw.JSONError{ Code: http.StatusBadRequest, Message: fmt.Sprintf("No kind %s found", string(cfg.Kind)), } @@ -276,7 +278,7 @@ ctx := req.Context() var tx *sql.Tx - if tx, err = JSONConn(req).BeginTx(ctx, nil); err != nil { + if tx, err = mw.JSONConn(req).BeginTx(ctx, nil); err != nil { return } defer tx.Rollback() @@ -304,20 +306,20 @@ ID: id, } - jr = JSONResult{ + jr = mw.JSONResult{ Code: http.StatusCreated, Result: &result, } return } -func listImportConfigs(req *http.Request) (jr JSONResult, err error) { +func listImportConfigs(req *http.Request) (jr mw.JSONResult, err error) { ctx := req.Context() configs := []*imports.ImportConfigOut{} if err = imports.ListAllPersistentConfigurationsContext( - ctx, JSONConn(req), + ctx, mw.JSONConn(req), func(config *imports.ImportConfigOut) error { configs = append(configs, config) return nil @@ -325,6 +327,6 @@ ); err != nil { return } - jr = JSONResult{Result: configs} + jr = mw.JSONResult{Result: configs} return }
--- a/pkg/controllers/importqueue.go Thu Aug 22 10:54:08 2019 +0200 +++ b/pkg/controllers/importqueue.go Thu Aug 22 11:26:48 2019 +0200 @@ -30,6 +30,8 @@ "gemma.intevation.de/gemma/pkg/common" "gemma.intevation.de/gemma/pkg/imports" "gemma.intevation.de/gemma/pkg/models" + + mw "gemma.intevation.de/gemma/pkg/middleware" ) const ( @@ -228,7 +230,7 @@ return &models.ImportTime{Time: when.UTC()} } -func listImports(req *http.Request) (jr JSONResult, err error) { +func listImports(req *http.Request) (jr mw.JSONResult, err error) { var list, before, after *filledStmt @@ -238,7 +240,7 @@ ctx := req.Context() - conn := JSONConn(req) + conn := mw.JSONConn(req) // Fast path for counting @@ -252,7 +254,7 @@ case err != nil: return } - jr = JSONResult{Result: count} + jr = mw.JSONResult{Result: count} return } @@ -305,7 +307,7 @@ next = neighbored(ctx, conn, after) } - jr = JSONResult{ + jr = mw.JSONResult{ Result: struct { Prev *models.ImportTime `json:"prev,omitempty"` Next *models.ImportTime `json:"next,omitempty"` @@ -319,13 +321,13 @@ return } -func importLogs(req *http.Request) (jr JSONResult, err error) { +func importLogs(req *http.Request) (jr mw.JSONResult, err error) { ctx := req.Context() id, _ := strconv.ParseInt(mux.Vars(req)["id"], 10, 64) - conn := JSONConn(req) + conn := mw.JSONConn(req) // Check if he have such a import job first. var summary sql.NullString @@ -336,7 +338,7 @@ ) switch { case err == sql.ErrNoRows: - err = JSONError{ + err = mw.JSONError{ Code: http.StatusNotFound, Message: fmt.Sprintf("Cannot find import #%d.", id), } @@ -378,7 +380,7 @@ return } - jr = JSONResult{ + jr = mw.JSONResult{ Result: struct { Enqueued models.ImportTime `json:"enqueued"` Summary interface{} `json:"summary,omitempty"` @@ -392,13 +394,13 @@ return } -func deleteImport(req *http.Request) (jr JSONResult, err error) { +func deleteImport(req *http.Request) (jr mw.JSONResult, err error) { ctx := req.Context() id, _ := strconv.ParseInt(mux.Vars(req)["id"], 10, 64) var tx *sql.Tx - tx, err = JSONConn(req).BeginTx(ctx, nil) + tx, err = mw.JSONConn(req).BeginTx(ctx, nil) if err != nil { return } @@ -409,7 +411,7 @@ err = tx.QueryRowContext(ctx, selectHasNoRunningImportSQL, id).Scan(&dummy) switch { case err == sql.ErrNoRows: - err = JSONError{ + err = mw.JSONError{ Code: http.StatusNotFound, Message: fmt.Sprintf("Cannot find import #%d.", id), } @@ -430,7 +432,7 @@ return } - jr = JSONResult{Code: http.StatusNoContent} + jr = mw.JSONResult{Code: http.StatusNoContent} return } @@ -456,9 +458,9 @@ INSERT INTO import.import_logs (import_id, msg) VALUES ($1, $2)` ) -func reviewImports(req *http.Request) (JSONResult, error) { +func reviewImports(req *http.Request) (mw.JSONResult, error) { - rs := *JSONInput(req).(*[]models.Review) + rs := *mw.JSONInput(req).(*[]models.Review) type reviewResult struct { ID int64 `json:"id"` @@ -468,7 +470,7 @@ results := make([]reviewResult, len(rs)) - conn := JSONConn(req) + conn := mw.JSONConn(req) for i := range rs { rev := &rs[i] @@ -484,17 +486,17 @@ } } - return JSONResult{Result: results}, nil + return mw.JSONResult{Result: results}, nil } -func reviewImport(req *http.Request) (jr JSONResult, err error) { +func reviewImport(req *http.Request) (jr mw.JSONResult, err error) { vars := mux.Vars(req) id, _ := strconv.ParseInt(vars["id"], 10, 64) state := vars["state"] var msg string - if msg, err = decideImport(req, JSONConn(req), id, state); err != nil { + if msg, err = decideImport(req, mw.JSONConn(req), id, state); err != nil { return } @@ -504,7 +506,7 @@ Message: msg, } - jr = JSONResult{Result: &result} + jr = mw.JSONResult{Result: &result} return } @@ -527,7 +529,7 @@ err = tx.QueryRowContext(ctx, isPendingSQL, id).Scan(&pending, &kind) switch { case err == sql.ErrNoRows: - err = JSONError{ + err = mw.JSONError{ Code: http.StatusNotFound, Message: fmt.Sprintf("cannot find import #%d", id), } @@ -535,7 +537,7 @@ case err != nil: return case !pending: - err = JSONError{ + err = mw.JSONError{ Code: http.StatusConflict, Message: fmt.Sprintf("import #%d is not pending", id), }
--- a/pkg/controllers/json.go Thu Aug 22 10:54:08 2019 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,208 +0,0 @@ -// This is Free Software under GNU Affero General Public License v >= 3.0 -// without warranty, see README.md and license for details. -// -// SPDX-License-Identifier: AGPL-3.0-or-later -// License-Filename: LICENSES/AGPL-3.0.txt -// -// Copyright (C) 2018 by via donau -// – Österreichische Wasserstraßen-Gesellschaft mbH -// Software engineering by Intevation GmbH -// -// Author(s): -// * Sascha L. Teichmann <sascha.teichmann@intevation.de> - -package controllers - -import ( - "context" - "database/sql" - "encoding/json" - "fmt" - "io" - "log" - "net/http" - - "github.com/jackc/pgx" - - "gemma.intevation.de/gemma/pkg/auth" -) - -// JSONResult defines the return type of JSONHandler handler function. -type JSONResult struct { - // Code is the HTTP status code to be set which defaults to http.StatusOK (200). - Code int - // Result is serialized to JSON. - // If the type is an io.Reader its copied through. - Result interface{} -} - -// JSONDefaultLimit is default size limit in bytes of an accepted -// input document. -const JSONDefaultLimit = 2048 - -// JSONHandler implements a middleware to ease the handing JSON input -// streams and return JSON documents as output. -type JSONHandler struct { - // Input (if not nil) is called to fill a data structure - // returned by this function. - Input func(*http.Request) interface{} - // Handle is called to handle the incoming HTTP request. - // in is the data structure returned by Input. Its nil if Input is nil. - Handle func(rep *http.Request) (JSONResult, error) - // NoConn if set to true no database connection is established and - // the conn parameter of the Handle call is nil. - NoConn bool - // Limit overides the default size of accepted input documents. - // Set to a negative value to allow an arbitrary size. - // Handle with care! - Limit int64 -} - -// JSONError is an error if returned by the JSONHandler.Handle function -// which ends up encoded as a JSON document. -type JSONError struct { - // Code is the HTTP status code of the result defaults - // to http.StatusInternalServerError if not set. - Code int - // The message of the error. - Message string -} - -// Error implements the error interface. -func (je JSONError) Error() string { - return fmt.Sprintf("%d: %s", je.Code, je.Message) -} - -type jsonHandlerType int - -const ( - jsonConnKey jsonHandlerType = iota - jsonInputKey -) - -// JSONConn extracts the impersonated sql.Conn from the context of the request. -func JSONConn(req *http.Request) *sql.Conn { - if conn, ok := req.Context().Value(jsonConnKey).(*sql.Conn); ok { - return conn - } - return nil -} - -// JSONConn extracts the de-serialized input from the context of the request. -func JSONInput(req *http.Request) interface{} { - return req.Context().Value(jsonInputKey) -} - -// ServeHTTP makes the JSONHandler a middleware. -func (j *JSONHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { - - if j.Input != nil { - input := j.Input(req) - defer req.Body.Close() - var r io.Reader - switch { - case j.Limit == 0: - r = io.LimitReader(req.Body, JSONDefaultLimit) - case j.Limit > 0: - r = io.LimitReader(req.Body, j.Limit) - default: - r = req.Body - } - if err := json.NewDecoder(r).Decode(input); err != nil { - http.Error(rw, "error: "+err.Error(), http.StatusBadRequest) - return - } - parent := req.Context() - ctx := context.WithValue(parent, jsonInputKey, input) - req = req.WithContext(ctx) - } - - var jr JSONResult - var err error - - if token, ok := auth.GetToken(req); ok && !j.NoConn { - if session := auth.Sessions.Session(token); session != nil { - parent := req.Context() - err = auth.RunAs(parent, session.User, func(conn *sql.Conn) error { - ctx := context.WithValue(parent, jsonConnKey, conn) - req = req.WithContext(ctx) - jr, err = j.Handle(req) - return err - }) - } else { - err = auth.ErrNoSuchToken - } - } else { - jr, err = j.Handle(req) - } - - if err != nil { - log.Printf("error: %v\n", err) - switch e := err.(type) { - case pgx.PgError: - var res = struct { - Result string `json:"result"` - Code string `json:"code"` - Message string `json:"message"` - }{ - Result: "failure", - Code: e.Code, - Message: e.Message, - } - rw.Header().Set("Content-Type", "application/json") - rw.WriteHeader(http.StatusInternalServerError) - if err := json.NewEncoder(rw).Encode(&res); err != nil { - log.Printf("error: %v\n", err) - } - case JSONError: - rw.Header().Set("Content-Type", "application/json") - if e.Code == 0 { - e.Code = http.StatusInternalServerError - } - rw.WriteHeader(e.Code) - var res = struct { - Message string `json:"message"` - }{ - Message: e.Message, - } - if err := json.NewEncoder(rw).Encode(&res); err != nil { - log.Printf("error: %v\n", err) - } - default: - http.Error(rw, - "error: "+err.Error(), - http.StatusInternalServerError) - } - return - } - - if jr.Code == 0 { - jr.Code = http.StatusOK - } - - if jr.Code != http.StatusNoContent { - rw.Header().Set("Content-Type", "application/json") - } - rw.WriteHeader(jr.Code) - if jr.Code != http.StatusNoContent { - var err error - if r, ok := jr.Result.(io.Reader); ok { - _, err = io.Copy(rw, r) - } else { - err = json.NewEncoder(rw).Encode(jr.Result) - } - if err != nil { - log.Printf("error: %v\n", err) - } - } -} - -// SendJSON sends data JSON encoded to the response writer -// with a given HTTP status code. -func SendJSON(rw http.ResponseWriter, code int, data interface{}) { - rw.Header().Set("Content-Type", "application/json") - rw.WriteHeader(code) - if err := json.NewEncoder(rw).Encode(data); err != nil { - log.Printf("error: %v\n", err) - } -}
--- a/pkg/controllers/manualimports.go Thu Aug 22 10:54:08 2019 +0200 +++ b/pkg/controllers/manualimports.go Thu Aug 22 11:26:48 2019 +0200 @@ -25,6 +25,8 @@ "gemma.intevation.de/gemma/pkg/common" "gemma.intevation.de/gemma/pkg/imports" "gemma.intevation.de/gemma/pkg/models" + + mw "gemma.intevation.de/gemma/pkg/middleware" ) func importModel(req *http.Request) interface{} { @@ -37,13 +39,13 @@ return ctor() } -func manualImport(req *http.Request) (jr JSONResult, err error) { +func manualImport(req *http.Request) (jr mw.JSONResult, err error) { kind := imports.JobKind(mux.Vars(req)["kind"]) - input := JSONInput(req) + input := mw.JSONInput(req) what := imports.ConvertToInternal(kind, input) if what == nil { - err = JSONError{ + err = mw.JSONError{ Code: http.StatusInternalServerError, Message: "Unable to convert import models", } @@ -100,7 +102,7 @@ ID: jobID, } - jr = JSONResult{ + jr = mw.JSONResult{ Code: http.StatusCreated, Result: &result, }
--- a/pkg/controllers/printtemplates.go Thu Aug 22 10:54:08 2019 +0200 +++ b/pkg/controllers/printtemplates.go Thu Aug 22 11:26:48 2019 +0200 @@ -25,6 +25,8 @@ "github.com/jackc/pgx/pgtype" "gemma.intevation.de/gemma/pkg/models" + + mw "gemma.intevation.de/gemma/pkg/middleware" ) const maxPrintTemplateSize = 5 * 1024 * 1024 @@ -70,7 +72,7 @@ var templateTypes = []string{"map", "diagram", "report"} -func listPrintTemplates(req *http.Request) (jr JSONResult, err error) { +func listPrintTemplates(req *http.Request) (jr mw.JSONResult, err error) { ts := mux.Vars(req)["type"] if ts == "" { @@ -90,7 +92,7 @@ stmt.WriteString(" ORDER BY date_info DESC") var rows *sql.Rows - if rows, err = JSONConn(req).QueryContext(req.Context(), stmt.String(), args...); err != nil { + if rows, err = mw.JSONConn(req).QueryContext(req.Context(), stmt.String(), args...); err != nil { return } defer rows.Close() @@ -123,22 +125,22 @@ templates = append(templates, &tmpl) } - jr = JSONResult{Result: templates} + jr = mw.JSONResult{Result: templates} return } -func fetchPrintTemplate(req *http.Request) (jr JSONResult, err error) { +func fetchPrintTemplate(req *http.Request) (jr mw.JSONResult, err error) { vars := mux.Vars(req) name, typ := vars["name"], vars["type"] ctx := req.Context() var data pgtype.Bytea - err = JSONConn(req).QueryRowContext(ctx, selectPrintTemplateSQL, name, typ).Scan(&data) + err = mw.JSONConn(req).QueryRowContext(ctx, selectPrintTemplateSQL, name, typ).Scan(&data) switch { case err == sql.ErrNoRows: - err = JSONError{ + err = mw.JSONError{ Code: http.StatusNotFound, Message: "No such template found", } @@ -146,32 +148,32 @@ case err != nil: return case data.Status != pgtype.Present: - err = JSONError{ + err = mw.JSONError{ Code: http.StatusInternalServerError, Message: "Unexpected return value from database query", } return } - jr = JSONResult{Result: bytes.NewReader(data.Bytes)} + jr = mw.JSONResult{Result: bytes.NewReader(data.Bytes)} return } -func createPrintTemplate(req *http.Request) (jr JSONResult, err error) { +func createPrintTemplate(req *http.Request) (jr mw.JSONResult, err error) { vars := mux.Vars(req) name, typ := vars["name"], vars["type"] - in := JSONInput(req).(*json.RawMessage) + in := mw.JSONInput(req).(*json.RawMessage) if name == "" { - err = JSONError{ + err = mw.JSONError{ Code: http.StatusBadRequest, Message: "Template must have a none empty name", } return } if len(*in) == 0 { - err = JSONError{ + err = mw.JSONError{ Code: http.StatusBadRequest, Message: "Template must have a none empty template", } @@ -180,7 +182,7 @@ ctx := req.Context() var tx *sql.Tx - if tx, err = JSONConn(req).BeginTx(ctx, nil); err != nil { + if tx, err = mw.JSONConn(req).BeginTx(ctx, nil); err != nil { return } defer tx.Rollback() @@ -194,7 +196,7 @@ case err != nil: return default: - err = JSONError{ + err = mw.JSONError{ Code: http.StatusBadRequest, Message: "A template with this name already exists", } @@ -209,7 +211,7 @@ if err = tx.Commit(); err != nil { return } - jr = JSONResult{ + jr = mw.JSONResult{ Code: http.StatusCreated, Result: map[string]string{ "created": name, @@ -218,14 +220,14 @@ return } -func deletePrintTemplate(req *http.Request) (jr JSONResult, err error) { +func deletePrintTemplate(req *http.Request) (jr mw.JSONResult, err error) { vars := mux.Vars(req) name, typ := vars["name"], vars["type"] ctx := req.Context() var tx *sql.Tx - if tx, err = JSONConn(req).BeginTx(ctx, nil); err != nil { + if tx, err = mw.JSONConn(req).BeginTx(ctx, nil); err != nil { return } defer tx.Rollback() @@ -235,7 +237,7 @@ switch { case err == sql.ErrNoRows: - err = JSONError{ + err = mw.JSONError{ Code: http.StatusNotFound, Message: "No such template found", } @@ -243,7 +245,7 @@ case err != nil: return case !dummy: - err = JSONError{ + err = mw.JSONError{ Code: http.StatusInternalServerError, Message: "Unexpected return value from database query", } @@ -258,7 +260,7 @@ return } - jr = JSONResult{ + jr = mw.JSONResult{ Result: map[string]string{ "deleted": name, }, @@ -267,22 +269,22 @@ return } -func updatePrintTemplate(req *http.Request) (jr JSONResult, err error) { +func updatePrintTemplate(req *http.Request) (jr mw.JSONResult, err error) { vars := mux.Vars(req) name, typ := vars["name"], vars["type"] - in := JSONInput(req).(*json.RawMessage) + in := mw.JSONInput(req).(*json.RawMessage) if name == "" { - err = JSONError{ + err = mw.JSONError{ Code: http.StatusBadRequest, Message: "Template must have a none empty name", } return } if len(*in) == 0 { - err = JSONError{ + err = mw.JSONError{ Code: http.StatusBadRequest, Message: "Template must have a none empty template", } @@ -291,7 +293,7 @@ ctx := req.Context() var tx *sql.Tx - if tx, err = JSONConn(req).BeginTx(ctx, nil); err != nil { + if tx, err = mw.JSONConn(req).BeginTx(ctx, nil); err != nil { return } defer tx.Rollback() @@ -301,7 +303,7 @@ switch { case err == sql.ErrNoRows: - err = JSONError{ + err = mw.JSONError{ Code: http.StatusNotFound, Message: "No such template found", } @@ -309,7 +311,7 @@ case err != nil: return case !dummy: - err = JSONError{ + err = mw.JSONError{ Code: http.StatusInternalServerError, Message: "Unexpected return value from database query", } @@ -325,7 +327,7 @@ return } - jr = JSONResult{ + jr = mw.JSONResult{ Code: http.StatusOK, Result: map[string]string{ "updated": name,
--- a/pkg/controllers/publish.go Thu Aug 22 10:54:08 2019 +0200 +++ b/pkg/controllers/publish.go Thu Aug 22 11:26:48 2019 +0200 @@ -17,10 +17,12 @@ "net/http" "gemma.intevation.de/gemma/pkg/models" + + mw "gemma.intevation.de/gemma/pkg/middleware" ) -func published(req *http.Request) (jr JSONResult, err error) { - jr = JSONResult{ +func published(req *http.Request) (mw.JSONResult, error) { + return mw.JSONResult{ Result: struct { Internal []models.IntEntry `json:"internal"` External []models.ExtEntry `json:"external"` @@ -28,6 +30,5 @@ Internal: models.InternalServices.Filter(models.InternalAll), External: models.ExternalServices.Filter(models.ExternalAll), }, - } - return + }, nil }
--- a/pkg/controllers/pwreset.go Thu Aug 22 10:54:08 2019 +0200 +++ b/pkg/controllers/pwreset.go Thu Aug 22 11:26:48 2019 +0200 @@ -39,6 +39,8 @@ "gemma.intevation.de/gemma/pkg/config" "gemma.intevation.de/gemma/pkg/misc" "gemma.intevation.de/gemma/pkg/models" + + mw "gemma.intevation.de/gemma/pkg/middleware" ) const ( @@ -235,7 +237,7 @@ return misc.SendMail(email, "Password Reset Link", body) } -func passwordResetRequest(req *http.Request) (jr JSONResult, err error) { +func passwordResetRequest(req *http.Request) (jr mw.JSONResult, err error) { // We do the checks and the emailing in background // no reduce the risks of timing attacks. @@ -245,7 +247,7 @@ if err := backgroundRequest(host, user); err != nil { log.Printf("error: %v\n", err) } - }(JSONInput(req).(*models.PWResetUser)) + }(mw.JSONInput(req).(*models.PWResetUser)) // Send a neutral message to avoid being an user oracle. const neutralMessage = "If this account exists, a reset link will be mailed."
--- a/pkg/controllers/routes.go Thu Aug 22 10:54:08 2019 +0200 +++ b/pkg/controllers/routes.go Thu Aug 22 11:26:48 2019 +0200 @@ -24,7 +24,7 @@ "gemma.intevation.de/gemma/pkg/auth" "gemma.intevation.de/gemma/pkg/imports" - "gemma.intevation.de/gemma/pkg/middleware" + mw "gemma.intevation.de/gemma/pkg/middleware" "gemma.intevation.de/gemma/pkg/models" ) @@ -40,56 +40,56 @@ ) // User management. - api.Handle("/users", any(&JSONHandler{ + api.Handle("/users", any(&mw.JSONHandler{ Handle: listUsers, })).Methods(http.MethodGet) - api.Handle("/users", sysAdmin(&JSONHandler{ + api.Handle("/users", sysAdmin(&mw.JSONHandler{ Input: func(*http.Request) interface{} { return new(models.User) }, Handle: createUser, })).Methods(http.MethodPost) - api.Handle("/users/{user}", any(&JSONHandler{ + api.Handle("/users/{user}", any(&mw.JSONHandler{ Handle: listUser, })).Methods(http.MethodGet) - api.Handle("/users/{user}", any(&JSONHandler{ + api.Handle("/users/{user}", any(&mw.JSONHandler{ Input: func(*http.Request) interface{} { return new(models.User) }, Handle: updateUser, })).Methods(http.MethodPut) - api.Handle("/users/{user}", sysAdmin(&JSONHandler{ + api.Handle("/users/{user}", sysAdmin(&mw.JSONHandler{ Handle: deleteUser, })).Methods(http.MethodDelete) // System notifications - api.Handle("/testmail/{user}", sysAdmin(&JSONHandler{ + api.Handle("/testmail/{user}", sysAdmin(&mw.JSONHandler{ Handle: sendTestMail, })).Methods(http.MethodGet) // System Management - api.Handle("/system/log/{service}/{file}", sysAdmin(&JSONHandler{ + api.Handle("/system/log/{service}/{file}", sysAdmin(&mw.JSONHandler{ Handle: showSystemLog, NoConn: true, })).Methods(http.MethodGet) // System Settings - api.Handle("/system/config", any(&JSONHandler{ + api.Handle("/system/config", any(&mw.JSONHandler{ Handle: getSystemConfig, NoConn: true, })).Methods(http.MethodGet) - api.Handle("/system/settings", any(&JSONHandler{ + api.Handle("/system/settings", any(&mw.JSONHandler{ Handle: getSystemSettings, })).Methods(http.MethodGet) - api.Handle("/system/settings", sysAdmin(&JSONHandler{ + api.Handle("/system/settings", sysAdmin(&mw.JSONHandler{ Input: func(*http.Request) interface{} { return &map[string]string{} }, Handle: setSystemSettings, })).Methods(http.MethodPut) // Password resets. - api.Handle("/users/passwordreset", &JSONHandler{ + api.Handle("/users/passwordreset", &mw.JSONHandler{ Input: func(*http.Request) interface{} { return new(models.PWResetUser) }, Handle: passwordResetRequest, NoConn: true, @@ -99,38 +99,38 @@ Methods(http.MethodGet) // Print templates - api.Handle("/templates", any(&JSONHandler{ + api.Handle("/templates", any(&mw.JSONHandler{ Handle: listPrintTemplates, })).Methods(http.MethodGet) tTypes := "{type:" + strings.Join(templateTypes, "|") + "}" - api.Handle("/templates/"+tTypes, any(&JSONHandler{ + api.Handle("/templates/"+tTypes, any(&mw.JSONHandler{ Handle: listPrintTemplates, })).Methods(http.MethodGet) - api.Handle("/templates/"+tTypes+"/{name}", any(&JSONHandler{ + api.Handle("/templates/"+tTypes+"/{name}", any(&mw.JSONHandler{ Handle: fetchPrintTemplate, })).Methods(http.MethodGet) - api.Handle("/templates/"+tTypes+"/{name}", waterwayAdmin(&JSONHandler{ + api.Handle("/templates/"+tTypes+"/{name}", waterwayAdmin(&mw.JSONHandler{ Input: func(*http.Request) interface{} { return &json.RawMessage{} }, Handle: createPrintTemplate, Limit: maxPrintTemplateSize, })).Methods(http.MethodPost) - api.Handle("/templates/"+tTypes+"/{name}", waterwayAdmin(&JSONHandler{ + api.Handle("/templates/"+tTypes+"/{name}", waterwayAdmin(&mw.JSONHandler{ Handle: deletePrintTemplate, })).Methods(http.MethodDelete) - api.Handle("/templates/"+tTypes+"/{name}", waterwayAdmin(&JSONHandler{ + api.Handle("/templates/"+tTypes+"/{name}", waterwayAdmin(&mw.JSONHandler{ Input: func(*http.Request) interface{} { return &json.RawMessage{} }, Handle: updatePrintTemplate, Limit: maxPrintTemplateSize, })).Methods(http.MethodPatch) // External proxies. - external := middleware.NotFound(&httputil.ReverseProxy{ + external := mw.NotFound(&httputil.ReverseProxy{ Director: proxyDirector(models.ExternalServices.Find), ModifyResponse: proxyModifyResponse("/api/external/"), }) @@ -148,13 +148,13 @@ http.MethodPut, http.MethodDelete) // Internal proxies. - internal := middleware.NotFound(&httputil.ReverseProxy{ + internal := mw.NotFound(&httputil.ReverseProxy{ Director: proxyDirector(models.InternalServices.Find), ModifyResponse: proxyModifyResponse("/api/internal/"), }) internalAuth := any( - middleware.ModifyQuery(internal, middleware.InjectUser)) + mw.ModifyQuery(internal, mw.InjectUser)) api.Handle("/internal/{hash}/{url}", internalAuth). Methods( @@ -166,35 +166,35 @@ http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete) - api.Handle("/published", any(&JSONHandler{ + api.Handle("/published", any(&mw.JSONHandler{ Handle: published, NoConn: true, })).Methods(http.MethodGet) // Survey selection - api.Handle("/surveys/{bottleneck}", any(&JSONHandler{ + api.Handle("/surveys/{bottleneck}", any(&mw.JSONHandler{ Handle: listSurveys, })).Methods(http.MethodGet) // Bottlenecks - api.Handle("/bottlenecks", any(&JSONHandler{ + api.Handle("/bottlenecks", any(&mw.JSONHandler{ Handle: listBottlenecks, })).Methods(http.MethodGet) // difference calculation - api.Handle("/diff", any(&JSONHandler{ + api.Handle("/diff", any(&mw.JSONHandler{ Input: func(*http.Request) interface{} { return new(models.DiffCalculationInput) }, Handle: diffCalculation, })).Methods(http.MethodPost) // Cross sections - api.Handle("/cross", any(&JSONHandler{ + api.Handle("/cross", any(&mw.JSONHandler{ Input: func(*http.Request) interface{} { return new(models.CrossSectionInput) }, Handle: crossSection, })).Methods(http.MethodPost) // Feature search - api.Handle("/search", any(&JSONHandler{ + api.Handle("/search", any(&mw.JSONHandler{ Input: func(*http.Request) interface{} { return new(models.SearchRequest) }, Handle: searchFeature, })).Methods(http.MethodPost) @@ -207,7 +207,7 @@ api.Handle("/imports/sr-upload/{token}", waterwayAdmin(http.HandlerFunc(deleteSoundingUpload))).Methods(http.MethodDelete) - api.Handle("/imports/sr-upload", waterwayAdmin(&JSONHandler{ + api.Handle("/imports/sr-upload", waterwayAdmin(&mw.JSONHandler{ Handle: uploadSoundingResult, })).Methods(http.MethodPost) @@ -229,7 +229,7 @@ api.Handle("/imports/ugm", waterwayAdmin( importUploadedGaugeMeasurement())).Methods(http.MethodPost) - api.Handle("/imports/{kind:st}", sysAdmin(&JSONHandler{ + api.Handle("/imports/{kind:st}", sysAdmin(&mw.JSONHandler{ Input: importModel, Handle: manualImport, NoConn: true, @@ -241,7 +241,7 @@ "sec", "dsec", }, "|") - api.Handle("/imports/{kind:"+kinds+"}", waterwayAdmin(&JSONHandler{ + api.Handle("/imports/{kind:"+kinds+"}", waterwayAdmin(&mw.JSONHandler{ Input: importModel, Handle: manualImport, NoConn: true, @@ -249,88 +249,88 @@ // Import scheduler configuration api.Handle("/imports/config/{id:[0-9]+}/run", - waterwayAdmin(&JSONHandler{ + waterwayAdmin(&mw.JSONHandler{ Handle: runImportConfig, })).Methods(http.MethodGet) api.Handle("/imports/config/{id:[0-9]+}", - waterwayAdmin(&JSONHandler{ + waterwayAdmin(&mw.JSONHandler{ Input: func(*http.Request) interface{} { return &json.RawMessage{} }, Handle: modifyImportConfig, })).Methods(http.MethodPatch) api.Handle("/imports/config/{id:[0-9]+}", - waterwayAdmin(&JSONHandler{ + waterwayAdmin(&mw.JSONHandler{ Handle: deleteImportConfig, })).Methods(http.MethodDelete) api.Handle("/imports/config/{id:[0-9]+}", - waterwayAdmin(&JSONHandler{ + waterwayAdmin(&mw.JSONHandler{ Handle: infoImportConfig, })).Methods(http.MethodGet) api.Handle("/imports/config", - waterwayAdmin(&JSONHandler{ + waterwayAdmin(&mw.JSONHandler{ Input: func(*http.Request) interface{} { return new(imports.ImportConfigIn) }, Handle: addImportConfig, })).Methods(http.MethodPost) api.Handle("/imports/config", - waterwayAdmin(&JSONHandler{ + waterwayAdmin(&mw.JSONHandler{ Handle: listImportConfigs, })).Methods(http.MethodGet) // Import queue - lsImports := waterwayAdmin(&JSONHandler{ + lsImports := waterwayAdmin(&mw.JSONHandler{ Handle: listImports, }) api.Handle("/imports", lsImports). Methods(http.MethodGet) - api.Handle("/imports/{id:[0-9]+}", waterwayAdmin(&JSONHandler{ + api.Handle("/imports/{id:[0-9]+}", waterwayAdmin(&mw.JSONHandler{ Handle: importLogs, })).Methods(http.MethodGet) - api.Handle("/imports", waterwayAdmin(&JSONHandler{ + api.Handle("/imports", waterwayAdmin(&mw.JSONHandler{ Input: func(*http.Request) interface{} { return &[]models.Review{} }, Handle: reviewImports, })).Methods(http.MethodPatch) - api.Handle("/imports/{id:[0-9]+}", waterwayAdmin(&JSONHandler{ + api.Handle("/imports/{id:[0-9]+}", waterwayAdmin(&mw.JSONHandler{ Handle: deleteImport, })).Methods(http.MethodDelete) // Handler to review an import which is pending. api.Handle("/imports/{id:[0-9]+}/{state:(?:accepted|declined)}", - waterwayAdmin(&JSONHandler{ + waterwayAdmin(&mw.JSONHandler{ Handle: reviewImport, })).Methods(http.MethodPut) // Handler to serve data to the client. api.Handle("/data/{kind:stretch|section}/availability/{name}", any( - middleware.DBConn(http.HandlerFunc(stretchAvailabilty)))).Methods(http.MethodGet) + mw.DBConn(http.HandlerFunc(stretchAvailabilty)))).Methods(http.MethodGet) api.Handle("/data/{kind:stretch|section}/fairway-depth/{name}", any( - middleware.DBConn(http.HandlerFunc(stretchAvailableFairwayDepth)))).Methods(http.MethodGet) + mw.DBConn(http.HandlerFunc(stretchAvailableFairwayDepth)))).Methods(http.MethodGet) api.Handle("/data/bottleneck/fairway-depth/{objnam}", any( - middleware.DBConn(http.HandlerFunc(bottleneckAvailableFairwayDepth)))).Methods(http.MethodGet) + mw.DBConn(http.HandlerFunc(bottleneckAvailableFairwayDepth)))).Methods(http.MethodGet) api.Handle("/data/bottleneck/availability/{objnam}", any( - middleware.DBConn(http.HandlerFunc(bottleneckAvailabilty)))).Methods(http.MethodGet) + mw.DBConn(http.HandlerFunc(bottleneckAvailabilty)))).Methods(http.MethodGet) api.Handle("/data/waterlevels/{gauge}", any( - middleware.DBConn(http.HandlerFunc(waterlevels)))).Methods(http.MethodGet) + mw.DBConn(http.HandlerFunc(waterlevels)))).Methods(http.MethodGet) api.Handle("/data/longterm-waterlevels/{gauge}", any( - middleware.DBConn(http.HandlerFunc(longtermWaterlevels)))).Methods(http.MethodGet) + mw.DBConn(http.HandlerFunc(longtermWaterlevels)))).Methods(http.MethodGet) api.Handle("/data/year-waterlevels/{gauge}/{year:[0-9]+}", any( - middleware.DBConn(http.HandlerFunc(yearWaterlevels)))).Methods(http.MethodGet) + mw.DBConn(http.HandlerFunc(yearWaterlevels)))).Methods(http.MethodGet) - api.Handle("/data/nash-sutcliffe/{gauge}", any(&JSONHandler{ + api.Handle("/data/nash-sutcliffe/{gauge}", any(&mw.JSONHandler{ Handle: nashSutcliffe, })).Methods(http.MethodGet)
--- a/pkg/controllers/search.go Thu Aug 22 10:54:08 2019 +0200 +++ b/pkg/controllers/search.go Thu Aug 22 11:26:48 2019 +0200 @@ -20,6 +20,8 @@ "strings" "gemma.intevation.de/gemma/pkg/models" + + mw "gemma.intevation.de/gemma/pkg/middleware" ) const ( @@ -38,18 +40,18 @@ ` ) -func searchFeature(req *http.Request) (jr JSONResult, err error) { +func searchFeature(req *http.Request) (jr mw.JSONResult, err error) { - s := JSONInput(req).(*models.SearchRequest) + s := mw.JSONInput(req).(*models.SearchRequest) if len(s.SearchString) == 0 { - err = JSONError{http.StatusBadRequest, + err = mw.JSONError{http.StatusBadRequest, "error: empty search string"} return } var result string - err = JSONConn(req).QueryRowContext( + err = mw.JSONConn(req).QueryRowContext( req.Context(), searchMostSQL, s.SearchString, @@ -63,15 +65,15 @@ return } -func listBottlenecks(req *http.Request) (jr JSONResult, err error) { +func listBottlenecks(req *http.Request) (jr mw.JSONResult, err error) { var result string - err = JSONConn(req).QueryRowContext( + err = mw.JSONConn(req).QueryRowContext( req.Context(), listBottlenecksSQL).Scan(&result) switch { case err == sql.ErrNoRows: - err = JSONError{ + err = mw.JSONError{ Code: http.StatusNotFound, Message: "Cannot find any bottleneck.", } @@ -80,6 +82,6 @@ return } - jr = JSONResult{Result: strings.NewReader(result)} + jr = mw.JSONResult{Result: strings.NewReader(result)} return }
--- a/pkg/controllers/srimports.go Thu Aug 22 10:54:08 2019 +0200 +++ b/pkg/controllers/srimports.go Thu Aug 22 11:26:48 2019 +0200 @@ -34,6 +34,8 @@ "gemma.intevation.de/gemma/pkg/imports" "gemma.intevation.de/gemma/pkg/misc" "gemma.intevation.de/gemma/pkg/models" + + mw "gemma.intevation.de/gemma/pkg/middleware" ) const ( @@ -176,7 +178,7 @@ }{ ID: jobID, } - SendJSON(rw, http.StatusCreated, &result) + mw.SendJSON(rw, http.StatusCreated, &result) } func loadMeta(f *zip.File) (*models.SoundingResultMeta, error) { @@ -189,7 +191,7 @@ return &m, m.Decode(r) } -func uploadSoundingResult(req *http.Request) (jr JSONResult, err error) { +func uploadSoundingResult(req *http.Request) (jr mw.JSONResult, err error) { var dir string if dir, err = misc.StoreUploadedFile( @@ -243,7 +245,7 @@ messages = append(messages, fmt.Sprintf("'meta.json' found but invalid: %v", err)) } else { - errs := meta.Validate(req.Context(), JSONConn(req)) + errs := meta.Validate(req.Context(), mw.JSONConn(req)) for _, err := range errs { messages = append(messages, fmt.Sprintf("invalid 'meta.json': %v", err)) @@ -274,7 +276,7 @@ result.Messages = messages - jr = JSONResult{ + jr = mw.JSONResult{ Code: code, Result: &result, } @@ -296,5 +298,5 @@ }{ Message: fmt.Sprintf("Token %s deleted.", token), } - SendJSON(rw, http.StatusOK, &result) + mw.SendJSON(rw, http.StatusOK, &result) }
--- a/pkg/controllers/surveys.go Thu Aug 22 10:54:08 2019 +0200 +++ b/pkg/controllers/surveys.go Thu Aug 22 11:26:48 2019 +0200 @@ -19,8 +19,11 @@ "database/sql" "net/http" + "github.com/gorilla/mux" + "gemma.intevation.de/gemma/pkg/models" - "github.com/gorilla/mux" + + mw "gemma.intevation.de/gemma/pkg/middleware" ) const ( @@ -41,13 +44,13 @@ WHERE b.objnam = $1 AND s.date_info::timestamptz <@ b.validity` ) -func listSurveys(req *http.Request) (jr JSONResult, err error) { +func listSurveys(req *http.Request) (jr mw.JSONResult, err error) { bottleneckName := mux.Vars(req)["bottleneck"] var rows *sql.Rows - rows, err = JSONConn(req).QueryContext(req.Context(), listSurveysSQL, bottleneckName) + rows, err = mw.JSONConn(req).QueryContext(req.Context(), listSurveysSQL, bottleneckName) if err != nil { return } @@ -80,7 +83,7 @@ return } - jr = JSONResult{ + jr = mw.JSONResult{ Result: struct { Surveys []*models.Survey `json:"surveys"` }{surveys},
--- a/pkg/controllers/system.go Thu Aug 22 10:54:08 2019 +0200 +++ b/pkg/controllers/system.go Thu Aug 22 11:26:48 2019 +0200 @@ -34,6 +34,8 @@ "gemma.intevation.de/gemma/pkg/geoserver" "gemma.intevation.de/gemma/pkg/imports" "gemma.intevation.de/gemma/pkg/models" + + mw "gemma.intevation.de/gemma/pkg/middleware" ) const ( @@ -57,7 +59,7 @@ // System status end points -func showSystemLog(req *http.Request) (jr JSONResult, err error) { +func showSystemLog(req *http.Request) (jr mw.JSONResult, err error) { serviceName := mux.Vars(req)["service"] fileName := mux.Vars(req)["file"] @@ -66,7 +68,7 @@ // able to inject a verbatim '/' via the middleware, but better be on // the safe site... if strings.Contains(fileName, "/") { - err = JSONError{http.StatusBadRequest, + err = mw.JSONError{http.StatusBadRequest, "error: no slashes allowed in file name"} return } @@ -77,7 +79,7 @@ case "apache2", "postgresql": path = "/var/log/" + serviceName + "/" + fileName default: - err = JSONError{http.StatusBadRequest, + err = mw.JSONError{http.StatusBadRequest, "error: invalid service: " + serviceName} return } @@ -88,7 +90,7 @@ return } - jr = JSONResult{ + jr = mw.JSONResult{ Result: struct { Path string `json:"path"` Content string `json:"content"` @@ -97,11 +99,11 @@ return } -func getSystemConfig(req *http.Request) (jr JSONResult, err error) { +func getSystemConfig(req *http.Request) (jr mw.JSONResult, err error) { cfg := config.PublishedConfig() if cfg == "" { - jr = JSONResult{Result: strings.NewReader("{}")} + jr = mw.JSONResult{Result: strings.NewReader("{}")} return } @@ -110,14 +112,14 @@ return } - jr = JSONResult{Result: bytes.NewReader(data)} + jr = mw.JSONResult{Result: bytes.NewReader(data)} return } -func getSystemSettings(req *http.Request) (jr JSONResult, err error) { +func getSystemSettings(req *http.Request) (jr mw.JSONResult, err error) { var rows *sql.Rows - if rows, err = JSONConn(req).QueryContext(req.Context(), getSettingsSQL); err != nil { + if rows, err = mw.JSONConn(req).QueryContext(req.Context(), getSettingsSQL); err != nil { return } defer rows.Close() @@ -135,7 +137,7 @@ return } - jr = JSONResult{Result: settings} + jr = mw.JSONResult{Result: settings} return } @@ -298,13 +300,13 @@ } } -func setSystemSettings(req *http.Request) (jr JSONResult, err error) { +func setSystemSettings(req *http.Request) (jr mw.JSONResult, err error) { - settings := JSONInput(req).(*map[string]string) + settings := mw.JSONInput(req).(*map[string]string) ctx := req.Context() var tx *sql.Tx - if tx, err = JSONConn(req).BeginTx(ctx, nil); err != nil { + if tx, err = mw.JSONConn(req).BeginTx(ctx, nil); err != nil { return } defer tx.Rollback() @@ -355,7 +357,7 @@ fn(req) } - jr = JSONResult{ + jr = mw.JSONResult{ Code: http.StatusCreated, Result: struct { Result string `json:"result"`
--- a/pkg/controllers/token.go Thu Aug 22 10:54:08 2019 +0200 +++ b/pkg/controllers/token.go Thu Aug 22 11:26:48 2019 +0200 @@ -21,6 +21,8 @@ "gemma.intevation.de/gemma/pkg/auth" "gemma.intevation.de/gemma/pkg/models" + + mw "gemma.intevation.de/gemma/pkg/middleware" ) func renew(rw http.ResponseWriter, req *http.Request) { @@ -50,7 +52,7 @@ Roles: session.Roles, } - SendJSON(rw, http.StatusOK, &result) + mw.SendJSON(rw, http.StatusOK, &result) } func logout(rw http.ResponseWriter, req *http.Request) { @@ -104,5 +106,5 @@ go deletePasswordResetRequest(session.User) - SendJSON(rw, http.StatusCreated, &result) + mw.SendJSON(rw, http.StatusCreated, &result) }
--- a/pkg/controllers/uploadedimports.go Thu Aug 22 10:54:08 2019 +0200 +++ b/pkg/controllers/uploadedimports.go Thu Aug 22 11:26:48 2019 +0200 @@ -24,6 +24,7 @@ "gemma.intevation.de/gemma/pkg/auth" "gemma.intevation.de/gemma/pkg/common" "gemma.intevation.de/gemma/pkg/imports" + mw "gemma.intevation.de/gemma/pkg/middleware" "gemma.intevation.de/gemma/pkg/misc" ) @@ -203,6 +204,6 @@ }{ ID: jobID, } - SendJSON(rw, http.StatusCreated, &result) + mw.SendJSON(rw, http.StatusCreated, &result) } }
--- a/pkg/controllers/user.go Thu Aug 22 10:54:08 2019 +0200 +++ b/pkg/controllers/user.go Thu Aug 22 11:26:48 2019 +0200 @@ -31,6 +31,8 @@ "gemma.intevation.de/gemma/pkg/models" "gemma.intevation.de/gemma/pkg/pgxutils" "gemma.intevation.de/gemma/pkg/scheduler" + + mw "gemma.intevation.de/gemma/pkg/middleware" ) const ( @@ -97,23 +99,23 @@ (inkluding import errors) and details on the concerned import.`)) ) -func deleteUser(req *http.Request) (jr JSONResult, err error) { +func deleteUser(req *http.Request) (jr mw.JSONResult, err error) { user := mux.Vars(req)["user"] if !models.UserName(user).IsValid() { - err = JSONError{http.StatusBadRequest, "error: user invalid"} + err = mw.JSONError{http.StatusBadRequest, "error: user invalid"} return } session, _ := auth.GetSession(req) if session.User == user { - err = JSONError{http.StatusBadRequest, "error: cannot delete yourself"} + err = mw.JSONError{http.StatusBadRequest, "error: cannot delete yourself"} return } ctx := req.Context() - db := JSONConn(req) + db := mw.JSONConn(req) // Remove scheduled tasks. ids, err2 := scheduler.ScheduledUserIDs(ctx, db, user) @@ -132,7 +134,7 @@ } if n, err2 := res.RowsAffected(); err2 == nil && n == 0 { - err = JSONError{ + err = mw.JSONError{ Code: http.StatusNotFound, Message: fmt.Sprintf("Cannot find user %s.", user), } @@ -142,22 +144,22 @@ // Running in a go routine should not be necessary. go func() { auth.Sessions.Logout(user) }() - jr = JSONResult{Code: http.StatusNoContent} + jr = mw.JSONResult{Code: http.StatusNoContent} return } -func updateUser(req *http.Request) (jr JSONResult, err error) { +func updateUser(req *http.Request) (jr mw.JSONResult, err error) { user := models.UserName(mux.Vars(req)["user"]) if !user.IsValid() { - err = JSONError{http.StatusBadRequest, "error: user invalid"} + err = mw.JSONError{http.StatusBadRequest, "error: user invalid"} return } - newUser := JSONInput(req).(*models.User) + newUser := mw.JSONInput(req).(*models.User) var res sql.Result - db := JSONConn(req) + db := mw.JSONConn(req) if s, _ := auth.GetSession(req); s.Roles.Has("sys_admin") { if newUser.Extent == nil { @@ -187,7 +189,7 @@ } } else { if newUser.Extent == nil { - err = JSONError{http.StatusBadRequest, "extent is mandatory"} + err = mw.JSONError{http.StatusBadRequest, "extent is mandatory"} return } res, err = db.ExecContext( @@ -206,7 +208,7 @@ } if n, err2 := res.RowsAffected(); err2 == nil && n == 0 { - err = JSONError{ + err = mw.JSONError{ Code: http.StatusNotFound, Message: fmt.Sprintf("Cannot find user %s.", user), } @@ -218,7 +220,7 @@ go func() { auth.Sessions.Logout(string(user)) }() } - jr = JSONResult{ + jr = mw.JSONResult{ Code: http.StatusCreated, Result: struct { Result string `json:"result"` @@ -227,11 +229,11 @@ return } -func createUser(req *http.Request) (jr JSONResult, err error) { +func createUser(req *http.Request) (jr mw.JSONResult, err error) { - user := JSONInput(req).(*models.User) + user := mw.JSONInput(req).(*models.User) - db := JSONConn(req) + db := mw.JSONConn(req) if user.Extent == nil { _, err = db.ExecContext( @@ -259,11 +261,11 @@ if err != nil { m, c := pgxutils.ReadableError{Err: err}.MessageAndCode() - err = JSONError{Code: c, Message: m} + err = mw.JSONError{Code: c, Message: m} return } - jr = JSONResult{ + jr = mw.JSONResult{ Code: http.StatusCreated, Result: struct { Result string `json:"result"` @@ -272,11 +274,11 @@ return } -func listUsers(req *http.Request) (jr JSONResult, err error) { +func listUsers(req *http.Request) (jr mw.JSONResult, err error) { var rows *sql.Rows - rows, err = JSONConn(req).QueryContext(req.Context(), listUsersSQL) + rows, err = mw.JSONConn(req).QueryContext(req.Context(), listUsersSQL) if err != nil { return } @@ -299,7 +301,7 @@ users = append(users, user) } - jr = JSONResult{ + jr = mw.JSONResult{ Result: struct { Users []*models.User `json:"users"` }{users}, @@ -307,11 +309,11 @@ return } -func listUser(req *http.Request) (jr JSONResult, err error) { +func listUser(req *http.Request) (jr mw.JSONResult, err error) { user := models.UserName(mux.Vars(req)["user"]) if !user.IsValid() { - err = JSONError{http.StatusBadRequest, "error: user invalid"} + err = mw.JSONError{http.StatusBadRequest, "error: user invalid"} return } @@ -320,7 +322,7 @@ Extent: &models.BoundingBox{}, } - err = JSONConn(req).QueryRowContext(req.Context(), listUserSQL, user).Scan( + err = mw.JSONConn(req).QueryRowContext(req.Context(), listUserSQL, user).Scan( &result.Role, &result.Country, &result.Email, @@ -330,7 +332,7 @@ switch { case err == sql.ErrNoRows: - err = JSONError{ + err = mw.JSONError{ Code: http.StatusNotFound, Message: fmt.Sprintf("Cannot find user %s.", user), } @@ -343,11 +345,11 @@ return } -func sendTestMail(req *http.Request) (jr JSONResult, err error) { +func sendTestMail(req *http.Request) (jr mw.JSONResult, err error) { user := models.UserName(mux.Vars(req)["user"]) if !user.IsValid() { - err = JSONError{http.StatusBadRequest, "error: user invalid"} + err = mw.JSONError{http.StatusBadRequest, "error: user invalid"} return } @@ -356,7 +358,7 @@ Extent: &models.BoundingBox{}, } - err = JSONConn(req).QueryRowContext(req.Context(), listUserSQL, user).Scan( + err = mw.JSONConn(req).QueryRowContext(req.Context(), listUserSQL, user).Scan( &userData.Role, &userData.Country, &userData.Email, @@ -366,7 +368,7 @@ switch { case err == sql.ErrNoRows: - err = JSONError{ + err = mw.JSONError{ Code: http.StatusNotFound, Message: fmt.Sprintf("Cannot find user %s.", user), } @@ -395,7 +397,7 @@ subject = "Gemma: Waterway Admin Notification TEST" bodyTmpl = testWWAdminNotifyMailTmpl } else { - err = JSONError{ + err = mw.JSONError{ Code: http.StatusBadRequest, Message: "Test mails can only be generated for admin roles.", }
--- a/pkg/middleware/json.go Thu Aug 22 10:54:08 2019 +0200 +++ b/pkg/middleware/json.go Thu Aug 22 11:26:48 2019 +0200 @@ -34,13 +34,13 @@ return req.Context().Value(jsonInputKey) } -// JSONInput is a middleware to deserialize the incomming +// JSONMiddleware is a middleware to deserialize the incomming // request body to a object to be created by a given input function // and stores the result into the context. // GetJSONInput can be used to receive the deserialized data. // limit limits the size of the incoming body to prevent // flooding the server. -func JSONInput(next http.Handler, input func(*http.Request) interface{}, limit int64) http.Handler { +func JSONMiddleware(next http.Handler, input func(*http.Request) interface{}, limit int64) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { dst := input(req)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pkg/middleware/jsonhandler.go Thu Aug 22 11:26:48 2019 +0200 @@ -0,0 +1,208 @@ +// This is Free Software under GNU Affero General Public License v >= 3.0 +// without warranty, see README.md and license for details. +// +// SPDX-License-Identifier: AGPL-3.0-or-later +// License-Filename: LICENSES/AGPL-3.0.txt +// +// Copyright (C) 2018 by via donau +// – Österreichische Wasserstraßen-Gesellschaft mbH +// Software engineering by Intevation GmbH +// +// Author(s): +// * Sascha L. Teichmann <sascha.teichmann@intevation.de> + +package middleware + +import ( + "context" + "database/sql" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + + "github.com/jackc/pgx" + + "gemma.intevation.de/gemma/pkg/auth" +) + +// JSONResult defines the return type of JSONHandler handler function. +type JSONResult struct { + // Code is the HTTP status code to be set which defaults to http.StatusOK (200). + Code int + // Result is serialized to JSON. + // If the type is an io.Reader its copied through. + Result interface{} +} + +// JSONDefaultLimit is default size limit in bytes of an accepted +// input document. +const JSONDefaultLimit = 2048 + +// JSONHandler implements a middleware to ease the handing JSON input +// streams and return JSON documents as output. +type JSONHandler struct { + // Input (if not nil) is called to fill a data structure + // returned by this function. + Input func(*http.Request) interface{} + // Handle is called to handle the incoming HTTP request. + // in is the data structure returned by Input. Its nil if Input is nil. + Handle func(rep *http.Request) (JSONResult, error) + // NoConn if set to true no database connection is established and + // the conn parameter of the Handle call is nil. + NoConn bool + // Limit overides the default size of accepted input documents. + // Set to a negative value to allow an arbitrary size. + // Handle with care! + Limit int64 +} + +// JSONError is an error if returned by the JSONHandler.Handle function +// which ends up encoded as a JSON document. +type JSONError struct { + // Code is the HTTP status code of the result defaults + // to http.StatusInternalServerError if not set. + Code int + // The message of the error. + Message string +} + +// Error implements the error interface. +func (je JSONError) Error() string { + return fmt.Sprintf("%d: %s", je.Code, je.Message) +} + +type jsonHandlerType int + +const ( + jsonHandlerConnKey jsonHandlerType = iota + jsonHandlerInputKey +) + +// JSONConn extracts the impersonated sql.Conn from the context of the request. +func JSONConn(req *http.Request) *sql.Conn { + if conn, ok := req.Context().Value(jsonHandlerConnKey).(*sql.Conn); ok { + return conn + } + return nil +} + +// JSONInput extracts the de-serialized input from the context of the request. +func JSONInput(req *http.Request) interface{} { + return req.Context().Value(jsonHandlerInputKey) +} + +// ServeHTTP makes the JSONHandler a middleware. +func (j *JSONHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + + if j.Input != nil { + input := j.Input(req) + defer req.Body.Close() + var r io.Reader + switch { + case j.Limit == 0: + r = io.LimitReader(req.Body, JSONDefaultLimit) + case j.Limit > 0: + r = io.LimitReader(req.Body, j.Limit) + default: + r = req.Body + } + if err := json.NewDecoder(r).Decode(input); err != nil { + http.Error(rw, "error: "+err.Error(), http.StatusBadRequest) + return + } + parent := req.Context() + ctx := context.WithValue(parent, jsonHandlerInputKey, input) + req = req.WithContext(ctx) + } + + var jr JSONResult + var err error + + if token, ok := auth.GetToken(req); ok && !j.NoConn { + if session := auth.Sessions.Session(token); session != nil { + parent := req.Context() + err = auth.RunAs(parent, session.User, func(conn *sql.Conn) error { + ctx := context.WithValue(parent, jsonHandlerConnKey, conn) + req = req.WithContext(ctx) + jr, err = j.Handle(req) + return err + }) + } else { + err = auth.ErrNoSuchToken + } + } else { + jr, err = j.Handle(req) + } + + if err != nil { + log.Printf("error: %v\n", err) + switch e := err.(type) { + case pgx.PgError: + var res = struct { + Result string `json:"result"` + Code string `json:"code"` + Message string `json:"message"` + }{ + Result: "failure", + Code: e.Code, + Message: e.Message, + } + rw.Header().Set("Content-Type", "application/json") + rw.WriteHeader(http.StatusInternalServerError) + if err := json.NewEncoder(rw).Encode(&res); err != nil { + log.Printf("error: %v\n", err) + } + case JSONError: + rw.Header().Set("Content-Type", "application/json") + if e.Code == 0 { + e.Code = http.StatusInternalServerError + } + rw.WriteHeader(e.Code) + var res = struct { + Message string `json:"message"` + }{ + Message: e.Message, + } + if err := json.NewEncoder(rw).Encode(&res); err != nil { + log.Printf("error: %v\n", err) + } + default: + http.Error(rw, + "error: "+err.Error(), + http.StatusInternalServerError) + } + return + } + + if jr.Code == 0 { + jr.Code = http.StatusOK + } + + if jr.Code != http.StatusNoContent { + rw.Header().Set("Content-Type", "application/json") + } + rw.WriteHeader(jr.Code) + if jr.Code != http.StatusNoContent { + var err error + if r, ok := jr.Result.(io.Reader); ok { + _, err = io.Copy(rw, r) + } else { + err = json.NewEncoder(rw).Encode(jr.Result) + } + if err != nil { + log.Printf("error: %v\n", err) + } + } +} + +// SendJSON sends data JSON encoded to the response writer +// with a given HTTP status code. +func SendJSON(rw http.ResponseWriter, code int, data interface{}) { + rw.Header().Set("Content-Type", "application/json") + rw.WriteHeader(code) + if err := json.NewEncoder(rw).Encode(data); err != nil { + log.Printf("error: %v\n", err) + } +}