changeset 4246:5e8dec0073ae

Merged json-handler-middleware into default.
author Sascha L. Teichmann <sascha.teichmann@intevation.de>
date Thu, 22 Aug 2019 11:57:58 +0200
parents 27434b0d4d96 (current diff) df198705300e (diff)
children 152b9eb5ca47
files pkg/controllers/json.go
diffstat 20 files changed, 471 insertions(+), 537 deletions(-) [+]
line wrap: on
line diff
--- a/pkg/controllers/cross.go	Thu Aug 22 10:56:05 2019 +0200
+++ b/pkg/controllers/cross.go	Thu Aug 22 11:57:58 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,16 +66,13 @@
 	return mls, err
 }
 
-func crossSection(
-	input interface{},
-	req *http.Request,
-	conn *sql.Conn,
-) (jr JSONResult, err error) {
+func crossSection(req *http.Request) (jr mw.JSONResult, err error) {
 
-	csi := input.(*models.CrossSectionInput)
+	csi := mw.JSONInput(req).(*models.CrossSectionInput)
 
 	start := time.Now()
 	ctx := req.Context()
+	conn := mw.JSONConn(req)
 
 	tree, err := octree.FromCache(
 		ctx, conn,
@@ -85,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,
@@ -159,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:56:05 2019 +0200
+++ b/pkg/controllers/diff.go	Thu Aug 22 11:57:58 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,19 +86,17 @@
 // TODO: Make this configurable?
 var diffCalculationSemaphore = semaphore.NewWeighted(int64(3))
 
-func diffCalculation(
-	input interface{},
-	req *http.Request,
-	conn *sql.Conn,
-) (jr JSONResult, err error) {
+func diffCalculation(req *http.Request) (jr mw.JSONResult, err error) {
 
 	begin := time.Now()
 	start := begin
 
-	dci := input.(*models.DiffCalculationInput)
+	dci := mw.JSONInput(req).(*models.DiffCalculationInput)
 
 	ctx := req.Context()
 
+	conn := mw.JSONConn(req)
+
 	var id int64
 	err = conn.QueryRowContext(
 		ctx,
@@ -113,7 +113,7 @@
 		return
 	default:
 		// We already have this diff
-		jr = JSONResult{
+		jr = mw.JSONResult{
 			Result: map[string]int64{"id": id},
 		}
 		return
@@ -135,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,
@@ -156,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,
@@ -167,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.",
@@ -317,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:56:05 2019 +0200
+++ b/pkg/controllers/gauges.go	Thu Aug 22 11:57:58 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,11 +575,8 @@
 	return values, nil
 }
 
-func nashSutcliffe(
-	_ interface{},
-	req *http.Request,
-	conn *sql.Conn,
-) (jr JSONResult, err error) {
+func nashSutcliffe(req *http.Request) (jr mw.JSONResult, err error) {
+
 	gauge := mux.Vars(req)["gauge"]
 
 	var isrs *models.Isrs
@@ -589,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),
 			}
@@ -604,7 +602,7 @@
 
 	var values []observedPredictedValues
 
-	if values, err = loadNashSutcliffeData(ctx, conn, isrs, when); err != nil {
+	if values, err = loadNashSutcliffeData(ctx, mw.JSONConn(req), isrs, when); err != nil {
 		return
 	}
 
@@ -643,7 +641,7 @@
 		observed = observed[:0]
 	}
 
-	jr = JSONResult{
+	jr = mw.JSONResult{
 		Result: &coeffs{
 			When:   models.ImportTime{Time: when},
 			Coeffs: cs,
@@ -729,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:56:05 2019 +0200
+++ b/pkg/controllers/importconfig.go	Thu Aug 22 11:57:58 2019 +0200
@@ -26,20 +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(
-	_ interface{},
-	req *http.Request,
-	conn *sql.Conn,
-) (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, conn, id); err != nil {
+	if jobID, err = imports.RunConfiguredImportContext(ctx, mw.JSONConn(req), id); err != nil {
 		return
 	}
 
@@ -49,30 +47,28 @@
 		ID: jobID,
 	}
 
-	jr = JSONResult{
+	jr = mw.JSONResult{
 		Code:   http.StatusCreated,
 		Result: &result,
 	}
 	return
 }
 
-func modifyImportConfig(
-	input interface{},
-	req *http.Request,
-	conn *sql.Conn,
-) (jr JSONResult, err error) {
+func modifyImportConfig(req *http.Request) (jr mw.JSONResult, err error) {
 
 	ctx := req.Context()
 
-	raw := input.(*json.RawMessage)
+	raw := mw.JSONInput(req).(*json.RawMessage)
 
 	id, _ := strconv.ParseInt(mux.Vars(req)["id"], 10, 64)
 
+	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),
 		}
@@ -84,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),
 		}
@@ -145,15 +141,11 @@
 		ID: id,
 	}
 
-	jr = JSONResult{Result: &result}
+	jr = mw.JSONResult{Result: &result}
 	return
 }
 
-func infoImportConfig(
-	_ interface{},
-	req *http.Request,
-	conn *sql.Conn,
-) (jr JSONResult, err error) {
+func infoImportConfig(req *http.Request) (jr mw.JSONResult, err error) {
 
 	ctx := req.Context()
 
@@ -161,12 +153,12 @@
 
 	var cfg *imports.PersistentConfig
 
-	cfg, err = imports.LoadPersistentConfigContext(ctx, conn, 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),
 		}
@@ -177,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),
 		}
@@ -200,7 +192,7 @@
 		return
 	}
 
-	jr = JSONResult{Result: &imports.ImportConfigOut{
+	jr = mw.JSONResult{Result: &imports.ImportConfigOut{
 		ID:     id,
 		Kind:   imports.ImportKind(cfg.Kind),
 		Config: what,
@@ -208,18 +200,14 @@
 	return
 }
 
-func deleteImportConfig(
-	_ interface{},
-	req *http.Request,
-	conn *sql.Conn,
-) (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 = conn.BeginTx(ctx, nil); err != nil {
+	if tx, err = mw.JSONConn(req).BeginTx(ctx, nil); err != nil {
 		return
 	}
 	defer tx.Rollback()
@@ -232,7 +220,7 @@
 
 	switch {
 	case err == sql.ErrNoRows:
-		err = JSONError{
+		err = mw.JSONError{
 			Code:    http.StatusNotFound,
 			Message: fmt.Sprintf("No configuration %d found", id),
 		}
@@ -254,24 +242,20 @@
 		ID: id,
 	}
 
-	jr = JSONResult{Result: &result}
+	jr = mw.JSONResult{Result: &result}
 
 	return
 }
 
-func addImportConfig(
-	input interface{},
-	req *http.Request,
-	conn *sql.Conn,
-) (jr JSONResult, err error) {
+func addImportConfig(req *http.Request) (jr mw.JSONResult, err error) {
 
-	cfg := input.(*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)),
 		}
@@ -294,7 +278,7 @@
 	ctx := req.Context()
 
 	var tx *sql.Tx
-	if tx, err = conn.BeginTx(ctx, nil); err != nil {
+	if tx, err = mw.JSONConn(req).BeginTx(ctx, nil); err != nil {
 		return
 	}
 	defer tx.Rollback()
@@ -322,24 +306,20 @@
 		ID: id,
 	}
 
-	jr = JSONResult{
+	jr = mw.JSONResult{
 		Code:   http.StatusCreated,
 		Result: &result,
 	}
 	return
 }
 
-func listImportConfigs(
-	_ interface{},
-	req *http.Request,
-	conn *sql.Conn,
-) (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, conn,
+		ctx, mw.JSONConn(req),
 		func(config *imports.ImportConfigOut) error {
 			configs = append(configs, config)
 			return nil
@@ -347,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:56:05 2019 +0200
+++ b/pkg/controllers/importqueue.go	Thu Aug 22 11:57:58 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,11 +230,7 @@
 	return &models.ImportTime{Time: when.UTC()}
 }
 
-func listImports(
-	_ interface{},
-	req *http.Request,
-	conn *sql.Conn,
-) (jr JSONResult, err error) {
+func listImports(req *http.Request) (jr mw.JSONResult, err error) {
 
 	var list, before, after *filledStmt
 
@@ -242,6 +240,8 @@
 
 	ctx := req.Context()
 
+	conn := mw.JSONConn(req)
+
 	// Fast path for counting
 
 	switch count := strings.ToLower(req.FormValue("count")); count {
@@ -254,7 +254,7 @@
 		case err != nil:
 			return
 		}
-		jr = JSONResult{Result: count}
+		jr = mw.JSONResult{Result: count}
 		return
 	}
 
@@ -307,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"`
@@ -321,16 +321,14 @@
 	return
 }
 
-func importLogs(
-	_ interface{},
-	req *http.Request,
-	conn *sql.Conn,
-) (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 := mw.JSONConn(req)
+
 	// Check if he have such a import job first.
 	var summary sql.NullString
 	var enqueued time.Time
@@ -340,7 +338,7 @@
 	)
 	switch {
 	case err == sql.ErrNoRows:
-		err = JSONError{
+		err = mw.JSONError{
 			Code:    http.StatusNotFound,
 			Message: fmt.Sprintf("Cannot find import #%d.", id),
 		}
@@ -382,7 +380,7 @@
 		return
 	}
 
-	jr = JSONResult{
+	jr = mw.JSONResult{
 		Result: struct {
 			Enqueued models.ImportTime        `json:"enqueued"`
 			Summary  interface{}              `json:"summary,omitempty"`
@@ -396,17 +394,13 @@
 	return
 }
 
-func deleteImport(
-	_ interface{},
-	req *http.Request,
-	conn *sql.Conn,
-) (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 = conn.BeginTx(ctx, nil)
+	tx, err = mw.JSONConn(req).BeginTx(ctx, nil)
 	if err != nil {
 		return
 	}
@@ -417,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),
 		}
@@ -438,7 +432,7 @@
 		return
 	}
 
-	jr = JSONResult{Code: http.StatusNoContent}
+	jr = mw.JSONResult{Code: http.StatusNoContent}
 
 	return
 }
@@ -464,13 +458,9 @@
 INSERT INTO import.import_logs (import_id, msg) VALUES ($1, $2)`
 )
 
-func reviewImports(
-	reviews interface{},
-	req *http.Request,
-	conn *sql.Conn,
-) (JSONResult, error) {
+func reviewImports(req *http.Request) (mw.JSONResult, error) {
 
-	rs := *reviews.(*[]models.Review)
+	rs := *mw.JSONInput(req).(*[]models.Review)
 
 	type reviewResult struct {
 		ID      int64  `json:"id"`
@@ -480,6 +470,8 @@
 
 	results := make([]reviewResult, len(rs))
 
+	conn := mw.JSONConn(req)
+
 	for i := range rs {
 		rev := &rs[i]
 		msg, err := decideImport(req, conn, rev.ID, string(rev.State))
@@ -494,21 +486,17 @@
 		}
 	}
 
-	return JSONResult{Result: results}, nil
+	return mw.JSONResult{Result: results}, nil
 }
 
-func reviewImport(
-	_ interface{},
-	req *http.Request,
-	conn *sql.Conn,
-) (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, conn, id, state); err != nil {
+	if msg, err = decideImport(req, mw.JSONConn(req), id, state); err != nil {
 		return
 	}
 
@@ -518,7 +506,7 @@
 		Message: msg,
 	}
 
-	jr = JSONResult{Result: &result}
+	jr = mw.JSONResult{Result: &result}
 	return
 }
 
@@ -541,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),
 		}
@@ -549,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:56:05 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,184 +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 (
-	"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.
-	// req is the incoming HTTP request.
-	// conn is the impersonated connection to the database.
-	Handle func(in interface{}, rep *http.Request, conn *sql.Conn) (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)
-}
-
-// ServeHTTP makes the JSONHandler a middleware.
-func (j *JSONHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
-
-	var input interface{}
-	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
-		}
-	}
-
-	var jr JSONResult
-	var err error
-
-	if token, ok := auth.GetToken(req); ok && !j.NoConn {
-		if session := auth.Sessions.Session(token); session != nil {
-			err = auth.RunAs(req.Context(), session.User, func(conn *sql.Conn) error {
-				jr, err = j.Handle(input, req, conn)
-				return err
-			})
-		} else {
-			err = auth.ErrNoSuchToken
-		}
-	} else {
-		jr, err = j.Handle(input, req, nil)
-	}
-
-	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:56:05 2019 +0200
+++ b/pkg/controllers/manualimports.go	Thu Aug 22 11:57:58 2019 +0200
@@ -15,16 +15,18 @@
 package controllers
 
 import (
-	"database/sql"
 	"log"
 	"net/http"
 	"time"
 
+	"github.com/gorilla/mux"
+
 	"gemma.intevation.de/gemma/pkg/auth"
 	"gemma.intevation.de/gemma/pkg/common"
 	"gemma.intevation.de/gemma/pkg/imports"
 	"gemma.intevation.de/gemma/pkg/models"
-	"github.com/gorilla/mux"
+
+	mw "gemma.intevation.de/gemma/pkg/middleware"
 )
 
 func importModel(req *http.Request) interface{} {
@@ -37,16 +39,13 @@
 	return ctor()
 }
 
-func manualImport(
-	input interface{},
-	req *http.Request,
-	_ *sql.Conn,
-) (jr JSONResult, err error) {
+func manualImport(req *http.Request) (jr mw.JSONResult, err error) {
 
 	kind := imports.JobKind(mux.Vars(req)["kind"])
+	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",
 		}
@@ -103,7 +102,7 @@
 		ID: jobID,
 	}
 
-	jr = JSONResult{
+	jr = mw.JSONResult{
 		Code:   http.StatusCreated,
 		Result: &result,
 	}
--- a/pkg/controllers/printtemplates.go	Thu Aug 22 10:56:05 2019 +0200
+++ b/pkg/controllers/printtemplates.go	Thu Aug 22 11:57:58 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,11 +72,7 @@
 
 var templateTypes = []string{"map", "diagram", "report"}
 
-func listPrintTemplates(
-	_ interface{},
-	req *http.Request,
-	conn *sql.Conn,
-) (jr JSONResult, err error) {
+func listPrintTemplates(req *http.Request) (jr mw.JSONResult, err error) {
 
 	ts := mux.Vars(req)["type"]
 	if ts == "" {
@@ -94,7 +92,7 @@
 	stmt.WriteString(" ORDER BY date_info DESC")
 
 	var rows *sql.Rows
-	if rows, err = conn.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()
@@ -127,26 +125,22 @@
 		templates = append(templates, &tmpl)
 	}
 
-	jr = JSONResult{Result: templates}
+	jr = mw.JSONResult{Result: templates}
 	return
 }
 
-func fetchPrintTemplate(
-	_ interface{},
-	req *http.Request,
-	conn *sql.Conn,
-) (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 = conn.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",
 		}
@@ -154,36 +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(
-	input interface{},
-	req *http.Request,
-	conn *sql.Conn,
-) (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 := input.(*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",
 		}
@@ -192,7 +182,7 @@
 
 	ctx := req.Context()
 	var tx *sql.Tx
-	if tx, err = conn.BeginTx(ctx, nil); err != nil {
+	if tx, err = mw.JSONConn(req).BeginTx(ctx, nil); err != nil {
 		return
 	}
 	defer tx.Rollback()
@@ -206,7 +196,7 @@
 	case err != nil:
 		return
 	default:
-		err = JSONError{
+		err = mw.JSONError{
 			Code:    http.StatusBadRequest,
 			Message: "A template with this name already exists",
 		}
@@ -221,7 +211,7 @@
 	if err = tx.Commit(); err != nil {
 		return
 	}
-	jr = JSONResult{
+	jr = mw.JSONResult{
 		Code: http.StatusCreated,
 		Result: map[string]string{
 			"created": name,
@@ -230,18 +220,14 @@
 	return
 }
 
-func deletePrintTemplate(
-	_ interface{},
-	req *http.Request,
-	conn *sql.Conn,
-) (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 = conn.BeginTx(ctx, nil); err != nil {
+	if tx, err = mw.JSONConn(req).BeginTx(ctx, nil); err != nil {
 		return
 	}
 	defer tx.Rollback()
@@ -251,7 +237,7 @@
 
 	switch {
 	case err == sql.ErrNoRows:
-		err = JSONError{
+		err = mw.JSONError{
 			Code:    http.StatusNotFound,
 			Message: "No such template found",
 		}
@@ -259,7 +245,7 @@
 	case err != nil:
 		return
 	case !dummy:
-		err = JSONError{
+		err = mw.JSONError{
 			Code:    http.StatusInternalServerError,
 			Message: "Unexpected return value from database query",
 		}
@@ -274,7 +260,7 @@
 		return
 	}
 
-	jr = JSONResult{
+	jr = mw.JSONResult{
 		Result: map[string]string{
 			"deleted": name,
 		},
@@ -283,26 +269,22 @@
 	return
 }
 
-func updatePrintTemplate(
-	input interface{},
-	req *http.Request,
-	conn *sql.Conn,
-) (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 := input.(*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",
 		}
@@ -311,7 +293,7 @@
 
 	ctx := req.Context()
 	var tx *sql.Tx
-	if tx, err = conn.BeginTx(ctx, nil); err != nil {
+	if tx, err = mw.JSONConn(req).BeginTx(ctx, nil); err != nil {
 		return
 	}
 	defer tx.Rollback()
@@ -321,7 +303,7 @@
 
 	switch {
 	case err == sql.ErrNoRows:
-		err = JSONError{
+		err = mw.JSONError{
 			Code:    http.StatusNotFound,
 			Message: "No such template found",
 		}
@@ -329,7 +311,7 @@
 	case err != nil:
 		return
 	case !dummy:
-		err = JSONError{
+		err = mw.JSONError{
 			Code:    http.StatusInternalServerError,
 			Message: "Unexpected return value from database query",
 		}
@@ -345,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:56:05 2019 +0200
+++ b/pkg/controllers/publish.go	Thu Aug 22 11:57:58 2019 +0200
@@ -14,14 +14,15 @@
 package controllers
 
 import (
-	"database/sql"
 	"net/http"
 
 	"gemma.intevation.de/gemma/pkg/models"
+
+	mw "gemma.intevation.de/gemma/pkg/middleware"
 )
 
-func published(_ interface{}, req *http.Request, _ *sql.Conn) (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"`
@@ -29,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:56:05 2019 +0200
+++ b/pkg/controllers/pwreset.go	Thu Aug 22 11:57:58 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,11 +237,7 @@
 	return misc.SendMail(email, "Password Reset Link", body)
 }
 
-func passwordResetRequest(
-	input interface{},
-	req *http.Request,
-	_ *sql.Conn,
-) (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.
@@ -249,7 +247,7 @@
 		if err := backgroundRequest(host, user); err != nil {
 			log.Printf("error: %v\n", err)
 		}
-	}(input.(*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:56:05 2019 +0200
+++ b/pkg/controllers/routes.go	Thu Aug 22 11:57:58 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:56:05 2019 +0200
+++ b/pkg/controllers/search.go	Thu Aug 22 11:57:58 2019 +0200
@@ -20,6 +20,8 @@
 	"strings"
 
 	"gemma.intevation.de/gemma/pkg/models"
+
+	mw "gemma.intevation.de/gemma/pkg/middleware"
 )
 
 const (
@@ -38,22 +40,18 @@
 `
 )
 
-func searchFeature(
-	input interface{},
-	req *http.Request,
-	db *sql.Conn,
-) (jr JSONResult, err error) {
+func searchFeature(req *http.Request) (jr mw.JSONResult, err error) {
 
-	s := input.(*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 = db.QueryRowContext(
+	err = mw.JSONConn(req).QueryRowContext(
 		req.Context(),
 		searchMostSQL,
 		s.SearchString,
@@ -67,19 +65,15 @@
 	return
 }
 
-func listBottlenecks(
-	_ interface{},
-	req *http.Request,
-	conn *sql.Conn,
-) (jr JSONResult, err error) {
+func listBottlenecks(req *http.Request) (jr mw.JSONResult, err error) {
 
 	var result string
-	err = conn.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.",
 		}
@@ -88,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:56:05 2019 +0200
+++ b/pkg/controllers/srimports.go	Thu Aug 22 11:57:58 2019 +0200
@@ -15,7 +15,6 @@
 
 import (
 	"archive/zip"
-	"database/sql"
 	"encoding/hex"
 	"fmt"
 	"log"
@@ -35,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 (
@@ -177,7 +178,7 @@
 	}{
 		ID: jobID,
 	}
-	SendJSON(rw, http.StatusCreated, &result)
+	mw.SendJSON(rw, http.StatusCreated, &result)
 }
 
 func loadMeta(f *zip.File) (*models.SoundingResultMeta, error) {
@@ -190,11 +191,7 @@
 	return &m, m.Decode(r)
 }
 
-func uploadSoundingResult(
-	_ interface{},
-	req *http.Request,
-	conn *sql.Conn,
-) (jr JSONResult, err error) {
+func uploadSoundingResult(req *http.Request) (jr mw.JSONResult, err error) {
 
 	var dir string
 	if dir, err = misc.StoreUploadedFile(
@@ -248,7 +245,7 @@
 				messages = append(messages,
 					fmt.Sprintf("'meta.json' found but invalid: %v", err))
 			} else {
-				errs := meta.Validate(req.Context(), conn)
+				errs := meta.Validate(req.Context(), mw.JSONConn(req))
 				for _, err := range errs {
 					messages = append(messages,
 						fmt.Sprintf("invalid 'meta.json': %v", err))
@@ -279,7 +276,7 @@
 
 	result.Messages = messages
 
-	jr = JSONResult{
+	jr = mw.JSONResult{
 		Code:   code,
 		Result: &result,
 	}
@@ -301,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:56:05 2019 +0200
+++ b/pkg/controllers/surveys.go	Thu Aug 22 11:57:58 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,17 +44,13 @@
 WHERE b.objnam = $1 AND s.date_info::timestamptz <@ b.validity`
 )
 
-func listSurveys(
-	_ interface{},
-	req *http.Request,
-	db *sql.Conn,
-) (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 = db.QueryContext(req.Context(), listSurveysSQL, bottleneckName)
+	rows, err = mw.JSONConn(req).QueryContext(req.Context(), listSurveysSQL, bottleneckName)
 	if err != nil {
 		return
 	}
@@ -84,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:56:05 2019 +0200
+++ b/pkg/controllers/system.go	Thu Aug 22 11:57:58 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,10 +59,7 @@
 
 // System status end points
 
-func showSystemLog(
-	_ interface{}, req *http.Request,
-	_ *sql.Conn,
-) (jr JSONResult, err error) {
+func showSystemLog(req *http.Request) (jr mw.JSONResult, err error) {
 
 	serviceName := mux.Vars(req)["service"]
 	fileName := mux.Vars(req)["file"]
@@ -69,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
 	}
@@ -80,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
 	}
@@ -91,7 +90,7 @@
 		return
 	}
 
-	jr = JSONResult{
+	jr = mw.JSONResult{
 		Result: struct {
 			Path    string `json:"path"`
 			Content string `json:"content"`
@@ -100,14 +99,11 @@
 	return
 }
 
-func getSystemConfig(
-	_ interface{}, req *http.Request,
-	_ *sql.Conn,
-) (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
 	}
 
@@ -116,18 +112,14 @@
 		return
 	}
 
-	jr = JSONResult{Result: bytes.NewReader(data)}
+	jr = mw.JSONResult{Result: bytes.NewReader(data)}
 	return
 }
 
-func getSystemSettings(
-	_ interface{},
-	req *http.Request,
-	conn *sql.Conn,
-) (jr JSONResult, err error) {
+func getSystemSettings(req *http.Request) (jr mw.JSONResult, err error) {
 
 	var rows *sql.Rows
-	if rows, err = conn.QueryContext(req.Context(), getSettingsSQL); err != nil {
+	if rows, err = mw.JSONConn(req).QueryContext(req.Context(), getSettingsSQL); err != nil {
 		return
 	}
 	defer rows.Close()
@@ -145,7 +137,7 @@
 		return
 	}
 
-	jr = JSONResult{Result: settings}
+	jr = mw.JSONResult{Result: settings}
 	return
 }
 
@@ -308,17 +300,13 @@
 	}
 }
 
-func setSystemSettings(
-	input interface{},
-	req *http.Request,
-	conn *sql.Conn,
-) (jr JSONResult, err error) {
+func setSystemSettings(req *http.Request) (jr mw.JSONResult, err error) {
 
-	settings := input.(*map[string]string)
+	settings := mw.JSONInput(req).(*map[string]string)
 
 	ctx := req.Context()
 	var tx *sql.Tx
-	if tx, err = conn.BeginTx(ctx, nil); err != nil {
+	if tx, err = mw.JSONConn(req).BeginTx(ctx, nil); err != nil {
 		return
 	}
 	defer tx.Rollback()
@@ -369,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:56:05 2019 +0200
+++ b/pkg/controllers/token.go	Thu Aug 22 11:57:58 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:56:05 2019 +0200
+++ b/pkg/controllers/uploadedimports.go	Thu Aug 22 11:57:58 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:56:05 2019 +0200
+++ b/pkg/controllers/user.go	Thu Aug 22 11:57:58 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,25 +99,24 @@
 (inkluding import errors) and details on the concerned import.`))
 )
 
-func deleteUser(
-	_ interface{}, req *http.Request,
-	db *sql.Conn,
-) (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 := mw.JSONConn(req)
+
 	// Remove scheduled tasks.
 	ids, err2 := scheduler.ScheduledUserIDs(ctx, db, user)
 	if err2 == nil {
@@ -133,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),
 		}
@@ -143,25 +144,23 @@
 	// 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(
-	input interface{},
-	req *http.Request,
-	db *sql.Conn,
-) (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 := input.(*models.User)
+	newUser := mw.JSONInput(req).(*models.User)
 	var res sql.Result
 
+	db := mw.JSONConn(req)
+
 	if s, _ := auth.GetSession(req); s.Roles.Has("sys_admin") {
 		if newUser.Extent == nil {
 			res, err = db.ExecContext(
@@ -190,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(
@@ -209,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),
 		}
@@ -221,7 +220,7 @@
 		go func() { auth.Sessions.Logout(string(user)) }()
 	}
 
-	jr = JSONResult{
+	jr = mw.JSONResult{
 		Code: http.StatusCreated,
 		Result: struct {
 			Result string `json:"result"`
@@ -230,13 +229,11 @@
 	return
 }
 
-func createUser(
-	input interface{},
-	req *http.Request,
-	db *sql.Conn,
-) (jr JSONResult, err error) {
+func createUser(req *http.Request) (jr mw.JSONResult, err error) {
 
-	user := input.(*models.User)
+	user := mw.JSONInput(req).(*models.User)
+
+	db := mw.JSONConn(req)
 
 	if user.Extent == nil {
 		_, err = db.ExecContext(
@@ -264,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"`
@@ -277,15 +274,11 @@
 	return
 }
 
-func listUsers(
-	_ interface{},
-	req *http.Request,
-	db *sql.Conn,
-) (jr JSONResult, err error) {
+func listUsers(req *http.Request) (jr mw.JSONResult, err error) {
 
 	var rows *sql.Rows
 
-	rows, err = db.QueryContext(req.Context(), listUsersSQL)
+	rows, err = mw.JSONConn(req).QueryContext(req.Context(), listUsersSQL)
 	if err != nil {
 		return
 	}
@@ -308,7 +301,7 @@
 		users = append(users, user)
 	}
 
-	jr = JSONResult{
+	jr = mw.JSONResult{
 		Result: struct {
 			Users []*models.User `json:"users"`
 		}{users},
@@ -316,15 +309,11 @@
 	return
 }
 
-func listUser(
-	_ interface{},
-	req *http.Request,
-	db *sql.Conn,
-) (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
 	}
 
@@ -333,7 +322,7 @@
 		Extent: &models.BoundingBox{},
 	}
 
-	err = db.QueryRowContext(req.Context(), listUserSQL, user).Scan(
+	err = mw.JSONConn(req).QueryRowContext(req.Context(), listUserSQL, user).Scan(
 		&result.Role,
 		&result.Country,
 		&result.Email,
@@ -343,7 +332,7 @@
 
 	switch {
 	case err == sql.ErrNoRows:
-		err = JSONError{
+		err = mw.JSONError{
 			Code:    http.StatusNotFound,
 			Message: fmt.Sprintf("Cannot find user %s.", user),
 		}
@@ -356,15 +345,11 @@
 	return
 }
 
-func sendTestMail(
-	_ interface{},
-	req *http.Request,
-	db *sql.Conn,
-) (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
 	}
 
@@ -373,7 +358,7 @@
 		Extent: &models.BoundingBox{},
 	}
 
-	err = db.QueryRowContext(req.Context(), listUserSQL, user).Scan(
+	err = mw.JSONConn(req).QueryRowContext(req.Context(), listUserSQL, user).Scan(
 		&userData.Role,
 		&userData.Country,
 		&userData.Email,
@@ -383,7 +368,7 @@
 
 	switch {
 	case err == sql.ErrNoRows:
-		err = JSONError{
+		err = mw.JSONError{
 			Code:    http.StatusNotFound,
 			Message: fmt.Sprintf("Cannot find user %s.", user),
 		}
@@ -412,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:56:05 2019 +0200
+++ b/pkg/middleware/json.go	Thu Aug 22 11:57:58 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:57:58 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)
+	}
+}