2019-05-16 10:39:25 -04:00
|
|
|
package web
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"encoding/json"
|
2019-05-18 18:06:10 -04:00
|
|
|
"fmt"
|
2019-05-16 10:39:25 -04:00
|
|
|
"net/http"
|
2019-05-18 18:06:10 -04:00
|
|
|
"os"
|
|
|
|
"path"
|
|
|
|
"path/filepath"
|
|
|
|
"strings"
|
2019-07-31 13:47:30 -08:00
|
|
|
|
2019-08-03 15:01:17 -08:00
|
|
|
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext"
|
2019-07-31 13:47:30 -08:00
|
|
|
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/weberror"
|
2019-05-18 18:06:10 -04:00
|
|
|
)
|
2019-05-16 10:39:25 -04:00
|
|
|
|
2019-05-18 18:06:10 -04:00
|
|
|
const (
|
|
|
|
charsetUTF8 = "charset=UTF-8"
|
|
|
|
)
|
|
|
|
|
|
|
|
// MIME types
|
|
|
|
const (
|
2019-05-20 22:16:58 -05:00
|
|
|
MIMEApplicationJSON = "application/json"
|
|
|
|
MIMEApplicationJSONCharsetUTF8 = MIMEApplicationJSON + "; " + charsetUTF8
|
|
|
|
MIMETextHTML = "text/html"
|
|
|
|
MIMETextHTMLCharsetUTF8 = MIMETextHTML + "; " + charsetUTF8
|
|
|
|
MIMETextPlain = "text/plain"
|
|
|
|
MIMETextPlainCharsetUTF8 = MIMETextPlain + "; " + charsetUTF8
|
|
|
|
MIMEOctetStream = "application/octet-stream"
|
2019-05-16 10:39:25 -04:00
|
|
|
)
|
|
|
|
|
2019-05-18 18:06:10 -04:00
|
|
|
// RespondJsonError sends an error formatted as JSON response back to the client.
|
2019-08-01 16:17:47 -08:00
|
|
|
func RespondJsonError(ctx context.Context, w http.ResponseWriter, er error) error {
|
2019-05-16 10:39:25 -04:00
|
|
|
|
2019-07-31 13:47:30 -08:00
|
|
|
// Set the status code for the request logger middleware.
|
|
|
|
// If the context is missing this value, request the service
|
|
|
|
// to be shutdown gracefully.
|
|
|
|
v, err := webcontext.ContextValues(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
2019-06-26 20:21:00 -08:00
|
|
|
}
|
|
|
|
|
2019-08-01 16:17:47 -08:00
|
|
|
webErr, ok := er.(*weberror.Error)
|
|
|
|
if !ok {
|
|
|
|
// If the error was of the type *Error, the handler has
|
|
|
|
// a specific status code and error to return.
|
|
|
|
webErr = weberror.NewError(ctx, er, v.StatusCode).(*weberror.Error)
|
|
|
|
}
|
|
|
|
|
2019-07-31 13:47:30 -08:00
|
|
|
v.StatusCode = webErr.Status
|
2019-05-16 10:39:25 -04:00
|
|
|
|
2019-08-03 15:01:17 -08:00
|
|
|
return RespondJson(ctx, w, webErr.Response(ctx, false), webErr.Status)
|
2019-05-16 10:39:25 -04:00
|
|
|
}
|
|
|
|
|
2019-05-18 18:06:10 -04:00
|
|
|
// RespondJson converts a Go value to JSON and sends it to the client.
|
2019-05-16 10:39:25 -04:00
|
|
|
// If code is StatusNoContent, v is expected to be nil.
|
2019-05-18 18:06:10 -04:00
|
|
|
func RespondJson(ctx context.Context, w http.ResponseWriter, data interface{}, statusCode int) error {
|
2019-05-16 10:39:25 -04:00
|
|
|
|
|
|
|
// Set the status code for the request logger middleware.
|
|
|
|
// If the context is missing this value, request the service
|
|
|
|
// to be shutdown gracefully.
|
2019-07-31 13:47:30 -08:00
|
|
|
v, err := webcontext.ContextValues(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
2019-05-16 10:39:25 -04:00
|
|
|
}
|
|
|
|
v.StatusCode = statusCode
|
|
|
|
|
|
|
|
// If there is nothing to marshal then set status code and return.
|
|
|
|
if statusCode == http.StatusNoContent {
|
|
|
|
w.WriteHeader(statusCode)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2019-06-24 17:36:42 -08:00
|
|
|
// Check to see if the json has already been encoded.
|
|
|
|
jsonData, ok := data.([]byte)
|
|
|
|
if !ok {
|
|
|
|
// Convert the response value to JSON.
|
|
|
|
var err error
|
|
|
|
jsonData, err = json.Marshal(data)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2019-05-16 10:39:25 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// Set the content type and headers once we know marshaling has succeeded.
|
2019-05-18 18:06:10 -04:00
|
|
|
w.Header().Set("Content-Type", MIMEApplicationJSONCharsetUTF8)
|
2019-05-16 10:39:25 -04:00
|
|
|
|
|
|
|
// Write the status code to the response.
|
|
|
|
w.WriteHeader(statusCode)
|
|
|
|
|
|
|
|
// Send the result back to the client.
|
|
|
|
if _, err := w.Write(jsonData); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
2019-05-18 18:06:10 -04:00
|
|
|
|
|
|
|
// RespondError sends an error back to the client as plain text with
|
|
|
|
// the status code 500 Internal Service Error
|
|
|
|
func RespondError(ctx context.Context, w http.ResponseWriter, er error) error {
|
2019-07-31 18:34:27 -08:00
|
|
|
return RespondErrorStatus(ctx, w, er, http.StatusInternalServerError)
|
2019-05-18 18:06:10 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// RespondErrorStatus sends an error back to the client as plain text with
|
|
|
|
// the specified HTTP status code.
|
|
|
|
func RespondErrorStatus(ctx context.Context, w http.ResponseWriter, er error, statusCode int) error {
|
2019-07-31 13:47:30 -08:00
|
|
|
|
|
|
|
// Set the status code for the request logger middleware.
|
|
|
|
// If the context is missing this value, request the service
|
|
|
|
// to be shutdown gracefully.
|
|
|
|
v, err := webcontext.ContextValues(ctx)
|
|
|
|
if err != nil {
|
2019-05-18 18:06:10 -04:00
|
|
|
return err
|
|
|
|
}
|
2019-07-31 13:47:30 -08:00
|
|
|
|
|
|
|
// If the error was of the type *Error, the handler has
|
|
|
|
// a specific status code and error to return.
|
|
|
|
webErr := weberror.NewError(ctx, er, v.StatusCode).(*weberror.Error)
|
|
|
|
v.StatusCode = webErr.Status
|
|
|
|
|
2019-08-03 15:01:17 -08:00
|
|
|
respErr := webErr.Response(ctx, false).String()
|
2019-07-31 13:47:30 -08:00
|
|
|
|
|
|
|
switch webcontext.ContextEnv(ctx) {
|
|
|
|
case webcontext.Env_Dev, webcontext.Env_Stage:
|
|
|
|
respErr = respErr + fmt.Sprintf("\n%s\n%+v", webErr.Error(), webErr.Cause)
|
|
|
|
}
|
|
|
|
|
|
|
|
return RespondText(ctx, w, respErr, statusCode)
|
2019-05-18 18:06:10 -04:00
|
|
|
}
|
|
|
|
|
2019-07-13 16:32:29 -08:00
|
|
|
// RespondText sends text back to the client as plain text with the specified HTTP status code.
|
|
|
|
func RespondText(ctx context.Context, w http.ResponseWriter, text string, statusCode int) error {
|
2019-07-31 13:47:30 -08:00
|
|
|
return Respond(ctx, w, []byte(text), statusCode, MIMETextPlainCharsetUTF8)
|
2019-07-13 16:32:29 -08:00
|
|
|
}
|
|
|
|
|
2019-05-18 18:06:10 -04:00
|
|
|
// Respond writes the data to the client with the specified HTTP status code and
|
|
|
|
// content type.
|
|
|
|
func Respond(ctx context.Context, w http.ResponseWriter, data []byte, statusCode int, contentType string) error {
|
2019-06-24 17:36:42 -08:00
|
|
|
|
2019-05-18 18:06:10 -04:00
|
|
|
// Set the status code for the request logger middleware.
|
|
|
|
// If the context is missing this value, request the service
|
|
|
|
// to be shutdown gracefully.
|
2019-07-31 13:47:30 -08:00
|
|
|
v, err := webcontext.ContextValues(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
2019-05-18 18:06:10 -04:00
|
|
|
}
|
|
|
|
v.StatusCode = statusCode
|
|
|
|
|
|
|
|
// If there is nothing to marshal then set status code and return.
|
|
|
|
if statusCode == http.StatusNoContent {
|
|
|
|
w.WriteHeader(statusCode)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Set the content type and headers once we know marshaling has succeeded.
|
|
|
|
w.Header().Set("Content-Type", contentType)
|
|
|
|
|
|
|
|
// Write the status code to the response.
|
|
|
|
w.WriteHeader(statusCode)
|
|
|
|
|
|
|
|
// Send the result back to the client.
|
|
|
|
if _, err := w.Write(data); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2019-07-31 13:47:30 -08:00
|
|
|
// RenderError sends an error back to the client as html with
|
|
|
|
// the specified HTTP status code.
|
|
|
|
func RenderError(ctx context.Context, w http.ResponseWriter, r *http.Request, er error, renderer Renderer, templateLayoutName, templateContentName, contentType string) error {
|
|
|
|
|
|
|
|
// Set the status code for the request logger middleware.
|
|
|
|
// If the context is missing this value, request the service
|
|
|
|
// to be shutdown gracefully.
|
|
|
|
v, err := webcontext.ContextValues(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2019-08-04 14:48:43 -08:00
|
|
|
webErr, ok := er.(*weberror.Error)
|
|
|
|
if !ok {
|
|
|
|
if v.StatusCode == 0 {
|
|
|
|
v.StatusCode = http.StatusInternalServerError
|
|
|
|
}
|
|
|
|
|
|
|
|
// If the error was of the type *Error, the handler has
|
|
|
|
// a specific status code and error to return.
|
|
|
|
webErr = weberror.NewError(ctx, er, v.StatusCode).(*weberror.Error)
|
|
|
|
}
|
|
|
|
v.StatusCode = webErr.Status
|
|
|
|
|
2019-08-12 21:28:16 -08:00
|
|
|
if RequestIsImage(r) {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2019-08-04 14:48:43 -08:00
|
|
|
resp := webErr.Response(ctx, true)
|
2019-07-31 13:47:30 -08:00
|
|
|
|
|
|
|
data := map[string]interface{}{
|
2019-08-04 14:48:43 -08:00
|
|
|
"StatusCode": resp.StatusCode,
|
|
|
|
"Error": resp.Error,
|
|
|
|
"Details": resp.Details,
|
|
|
|
"Fields": resp.Fields,
|
2019-07-31 13:47:30 -08:00
|
|
|
}
|
|
|
|
|
2019-08-04 14:48:43 -08:00
|
|
|
return renderer.Render(ctx, w, r, templateLayoutName, templateContentName, contentType, webErr.Status, data)
|
2019-07-31 13:47:30 -08:00
|
|
|
}
|
|
|
|
|
2019-05-18 18:06:10 -04:00
|
|
|
// Static registers a new route with path prefix to serve static files from the
|
|
|
|
// provided root directory. All errors will result in 404 File Not Found.
|
|
|
|
func Static(rootDir, prefix string) Handler {
|
|
|
|
h := func(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
|
|
|
err := StaticHandler(ctx, w, r, params, rootDir, prefix)
|
|
|
|
if err != nil {
|
|
|
|
return RespondErrorStatus(ctx, w, err, http.StatusNotFound)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return h
|
|
|
|
}
|
|
|
|
|
|
|
|
// StaticHandler sends a static file wo the client. The error is returned directly
|
|
|
|
// from this function allowing it to be wrapped by a Handler. The handler then was the
|
|
|
|
// the ability to format/display the error before responding to the client.
|
|
|
|
func StaticHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string, rootDir, prefix string) error {
|
|
|
|
// Parse the URL from the http request.
|
2019-05-20 22:16:58 -05:00
|
|
|
urlPath := path.Clean("/" + r.URL.Path) // "/"+ for security
|
2019-05-18 18:06:10 -04:00
|
|
|
urlPath = strings.TrimLeft(urlPath, "/")
|
|
|
|
|
|
|
|
// Remove the static directory name from the url
|
2019-05-20 22:16:58 -05:00
|
|
|
rootDirName := filepath.Base(rootDir)
|
|
|
|
if strings.HasPrefix(urlPath, rootDirName) {
|
|
|
|
urlPath = strings.Replace(urlPath, rootDirName, "", 1)
|
|
|
|
}
|
2019-05-18 18:06:10 -04:00
|
|
|
|
|
|
|
// Also remove the URL prefix used to serve the static file since
|
|
|
|
// this does not need to match any existing directory structure.
|
|
|
|
if prefix != "" {
|
|
|
|
urlPath = strings.TrimLeft(urlPath, prefix)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Resolve the root directory to an absolute path
|
|
|
|
sd, err := filepath.Abs(rootDir)
|
2019-05-20 22:16:58 -05:00
|
|
|
if err != nil {
|
2019-05-18 18:06:10 -04:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Append the requested file to the root directory
|
|
|
|
filePath := filepath.Join(sd, urlPath)
|
|
|
|
|
|
|
|
// Make sure the file exists before attempting to serve it so
|
|
|
|
// have the opportunity to handle the when a file does not exist.
|
|
|
|
if _, err := os.Stat(filePath); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Serve the file from the local file system.
|
2019-05-20 22:16:58 -05:00
|
|
|
http.ServeFile(w, r, filePath)
|
2019-05-18 18:06:10 -04:00
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
2019-08-05 18:47:42 -08:00
|
|
|
|
|
|
|
// Redirect ensures the session is flushed to the browser before the redirect is issued.
|
|
|
|
func Redirect(ctx context.Context, w http.ResponseWriter, r *http.Request, url string, code int) error {
|
|
|
|
if sess := webcontext.ContextSession(ctx); sess != nil {
|
|
|
|
if err := sess.Save(r, w); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
http.Redirect(w, r, url, code)
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|