From 32cb554dfad308c04061d928c22ff1e3c6537bc1 Mon Sep 17 00:00:00 2001 From: Lee Brown Date: Sat, 3 Aug 2019 15:01:17 -0800 Subject: [PATCH] completed flash messages --- cmd/web-app/handlers/examples.go | 136 ++++++++ cmd/web-app/handlers/routes.go | 11 +- cmd/web-app/handlers/signup.go | 5 + cmd/web-app/handlers/user.go | 7 +- .../templates/content/error-generic.gohtml | 10 +- .../content/examples-flash-messages.gohtml | 25 ++ .../templates/content/examples-images.gohtml | 42 +++ .../templates/content/signup-step1.gohtml | 3 +- .../templates/content/user-login.gohtml | 3 +- .../content/user-reset-confirm.gohtml | 3 +- .../content/user-reset-password.gohtml | 3 +- cmd/web-app/templates/layouts/base.gohtml | 21 +- .../templates/partials/page-wrapper.tmpl | 2 +- cmd/web-app/templates/partials/sidebar.tmpl | 80 +++-- cmd/web-app/templates/partials/topbar.tmpl | 320 +++++++++--------- internal/platform/web/response.go | 34 +- .../template-renderer/template_renderer.go | 20 +- .../platform/web/webcontext/flash_message.go | 81 +++++ internal/platform/web/webcontext/session.go | 9 +- .../platform/web/webcontext/transvalidate.go | 1 - internal/platform/web/weberror/error.go | 70 +++- internal/platform/web/weberror/validation.go | 11 +- 22 files changed, 653 insertions(+), 244 deletions(-) create mode 100644 cmd/web-app/handlers/examples.go create mode 100644 cmd/web-app/templates/content/examples-flash-messages.gohtml create mode 100644 cmd/web-app/templates/content/examples-images.gohtml create mode 100644 internal/platform/web/webcontext/flash_message.go diff --git a/cmd/web-app/handlers/examples.go b/cmd/web-app/handlers/examples.go new file mode 100644 index 0000000..a913fd8 --- /dev/null +++ b/cmd/web-app/handlers/examples.go @@ -0,0 +1,136 @@ +package handlers + +import ( + "context" + "net/http" + + "geeks-accelerator/oss/saas-starter-kit/internal/platform/web" + "geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext" + "geeks-accelerator/oss/saas-starter-kit/internal/platform/web/weberror" + "github.com/gorilla/schema" + "github.com/pkg/errors" +) + +// Example represents the example pages +type Examples struct { + Renderer web.Renderer +} + +// FlashMessages provides examples for displaying flash messages. +func (h *Examples) FlashMessages(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error { + + // Display example messages only when we aren't handling an example form post. + if r.Method == http.MethodGet { + + // Example displaying a success message. + webcontext.SessionFlashSuccess(ctx, + "Action Successful", + "You have reached an epic milestone.", + "800 hours", "304,232 lines of code") + + // Example displaying an info message. + webcontext.SessionFlashInfo(ctx, + "Take the Tour", + "Learn more about the platform...", + "The pretty little trees in the forest.", "The happy little clouds in the sky.") + + // Example displaying a warning message. + webcontext.SessionFlashWarning(ctx, + "Approaching Limit", + "Your account is reaching is limit, apply now!", + "Only overt benefit..") + + // Example displaying an error message. + webcontext.SessionFlashError(ctx, + "Custom Error", + "SOMETIMES ITS HELPFUL TO SHOUT.", + "Listen to me.", "Leaders don't follow.") + + // Example displaying a validation error which will use the json tag as the field name. + type valDemo struct { + Email string `json:"email_field_name" validate:"required,email"` + } + valErr := webcontext.Validator().StructCtx(ctx, valDemo{}) + weberror.SessionFlashError(ctx, valErr) + + // Generic error message for examples. + er := errors.New("Root causing undermined. Bailing out.") + + // Example displaying a flash message for a web error with a message. + webErrWithMsg := weberror.WithMessage(ctx, er, "weberror:WithMessage") + weberror.SessionFlashError(ctx, webErrWithMsg) + + // Example displaying a flash message for a web error. + webErr := weberror.NewError(ctx, er, http.StatusInternalServerError) + weberror.SessionFlashError(ctx, webErr) + + // Example displaying a flash message for an error with a message. + erWithMsg := errors.WithMessage(er, "pkg/errors:WithMessage") + weberror.SessionFlashError(ctx, erWithMsg) + + // Example displaying a flash message for an error that has been wrapped. + erWrap := errors.Wrap(er, "pkg/errors:Wrap") + weberror.SessionFlashError(ctx, erWrap) + } + + data := make(map[string]interface{}) + + // Example displaying a validation error which will use the json tag as the field name. + { + type inlineDemo struct { + Email string `json:"email" validate:"required,email"` + HiddenField string `json:"hidden_field" validate:"required"` + } + + req := new(inlineDemo) + f := func() error { + + if r.Method == http.MethodPost { + err := r.ParseForm() + if err != nil { + return err + } + + decoder := schema.NewDecoder() + if err := decoder.Decode(req, r.PostForm); err != nil { + return err + } + + if err := webcontext.Validator().Struct(req); err != nil { + if ne, ok := weberror.NewValidationError(ctx, err); ok { + data["validationErrors"] = ne.(*weberror.Error) + return nil + } else { + return err + } + } + } + + return nil + } + + if err := f(); err != nil { + return web.RenderError(ctx, w, r, err, h.Renderer, tmplLayoutBase, tmplContentErrorGeneric, web.MIMETextHTMLCharsetUTF8) + } + + data["form"] = req + + if verr, ok := weberror.NewValidationError(ctx, webcontext.Validator().Struct(inlineDemo{})); ok { + data["validationDefaults"] = verr.(*weberror.Error) + } + } + + return h.Renderer.Render(ctx, w, r, tmplLayoutBase, "examples-flash-messages.gohtml", web.MIMETextHTMLCharsetUTF8, http.StatusOK, data) +} + +// Images provides examples for responsive images that are auto re-sized. +func (h *Examples) Images(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error { + + // List of image sizes that will be used to resize the source image into. The resulting images will then be included + // as apart of the image src tag for a responsive image tag. + data := map[string]interface{}{ + "imgSizes": []int{100, 200, 300, 400, 500}, + } + + return h.Renderer.Render(ctx, w, r, tmplLayoutBase, "examples-images.gohtml", web.MIMETextHTMLCharsetUTF8, http.StatusOK, data) +} diff --git a/cmd/web-app/handlers/routes.go b/cmd/web-app/handlers/routes.go index fb04b51..c3a7261 100644 --- a/cmd/web-app/handlers/routes.go +++ b/cmd/web-app/handlers/routes.go @@ -3,13 +3,13 @@ package handlers import ( "context" "fmt" - "geeks-accelerator/oss/saas-starter-kit/internal/platform/notify" "log" "net/http" "os" "geeks-accelerator/oss/saas-starter-kit/internal/mid" "geeks-accelerator/oss/saas-starter-kit/internal/platform/auth" + "geeks-accelerator/oss/saas-starter-kit/internal/platform/notify" "geeks-accelerator/oss/saas-starter-kit/internal/platform/web" "geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext" "geeks-accelerator/oss/saas-starter-kit/internal/platform/web/weberror" @@ -74,6 +74,15 @@ func APP(shutdown chan os.Signal, log *log.Logger, env webcontext.Env, staticDir app.Handle("POST", "/signup", s.Step1) app.Handle("GET", "/signup", s.Step1) + // Register example endpoints. + ex := Examples{ + Renderer: renderer, + } + // This route is not authenticated + app.Handle("POST", "/examples/flash-messages", ex.FlashMessages) + app.Handle("GET", "/examples/flash-messages", ex.FlashMessages) + app.Handle("GET", "/examples/images", ex.Images) + // Register geo g := Geo{ MasterDB: masterDB, diff --git a/cmd/web-app/handlers/signup.go b/cmd/web-app/handlers/signup.go index 90bf220..023ec4a 100644 --- a/cmd/web-app/handlers/signup.go +++ b/cmd/web-app/handlers/signup.go @@ -79,6 +79,11 @@ func (h *Signup) Step1(ctx context.Context, w http.ResponseWriter, r *http.Reque return err } + // Display a welcome message to the user. + webcontext.SessionFlashSuccess(ctx, + "Thank you for Joining", + "You workflow will be a breeze starting today.") + // Redirect the user to the dashboard. http.Redirect(w, r, "/", http.StatusFound) return nil diff --git a/cmd/web-app/handlers/user.go b/cmd/web-app/handlers/user.go index a00aa02..d398a64 100644 --- a/cmd/web-app/handlers/user.go +++ b/cmd/web-app/handlers/user.go @@ -2,6 +2,7 @@ package handlers import ( "context" + "fmt" "geeks-accelerator/oss/saas-starter-kit/internal/platform/notify" project_routes "geeks-accelerator/oss/saas-starter-kit/internal/project-routes" "net/http" @@ -206,7 +207,11 @@ func (h *User) ResetPassword(ctx context.Context, w http.ResponseWriter, r *http } } - // Display a flash message!!! + // Display a success message to the user to check their email. + webcontext.SessionFlashSuccess(ctx, + "Check your email", + fmt.Sprintf("An email was sent to '%s'. Click on the link in the email to finish resetting your password.", req.Email)) + } return nil diff --git a/cmd/web-app/templates/content/error-generic.gohtml b/cmd/web-app/templates/content/error-generic.gohtml index cd6abe3..c499ddd 100644 --- a/cmd/web-app/templates/content/error-generic.gohtml +++ b/cmd/web-app/templates/content/error-generic.gohtml @@ -1,4 +1,4 @@ -{{define "title"}}Error {{ .statusCode }}{{end}} +{{define "title"}}Error {{ .StatusCode }}{{end}} {{define "style"}} {{end}} @@ -7,10 +7,10 @@
-
{{ .statusCode }}
-

{{ .errorMessage }}

- {{ if .fullError }} -

{{ .fullError }}

+
{{ .StatusCode }}
+

{{ .Error }}

+ {{ if .Details }} +

{{ .Details }}

{{ end }}
diff --git a/cmd/web-app/templates/content/examples-flash-messages.gohtml b/cmd/web-app/templates/content/examples-flash-messages.gohtml new file mode 100644 index 0000000..d943c94 --- /dev/null +++ b/cmd/web-app/templates/content/examples-flash-messages.gohtml @@ -0,0 +1,25 @@ +{{define "title"}}Example - Flash Messages{{end}} +{{define "style"}} + +{{end}} +{{define "content"}} + +

Inline Validation Example

+

Any field error that is not displayed inline will still be displayed as apart of the the validation at the top of the page.

+
+
+ + {{template "invalid-feedback" dict "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors "fieldName" "Email" }} +
+ + +
+
+ + +{{end}} +{{define "js"}} + +{{end}} diff --git a/cmd/web-app/templates/content/examples-images.gohtml b/cmd/web-app/templates/content/examples-images.gohtml new file mode 100644 index 0000000..38b02e2 --- /dev/null +++ b/cmd/web-app/templates/content/examples-images.gohtml @@ -0,0 +1,42 @@ +{{define "title"}}Example - Responsive Images{{end}} +{{define "style"}} + +{{end}} +{{define "content"}} + +

S3ImgSrcLarge + +

+ +

S3ImgThumbSrcLarge + +

+ +

S3ImgSrcMedium + +

+ +

S3ImgThumbSrcMedium + +

+ +

S3ImgSrcSmall + +

+ +

S3ImgThumbSrcSmall + +

+ +

S3ImgSrc + +

+ +

S3ImgUrl + +

+ +{{end}} +{{define "js"}} + +{{end}} diff --git a/cmd/web-app/templates/content/signup-step1.gohtml b/cmd/web-app/templates/content/signup-step1.gohtml index d804ef0..744be01 100644 --- a/cmd/web-app/templates/content/signup-step1.gohtml +++ b/cmd/web-app/templates/content/signup-step1.gohtml @@ -12,11 +12,12 @@
+ {{ template "app-flashes" . }} +

Create an Account!

- {{ template "top-error" . }} {{ template "validation-error" . }}
diff --git a/cmd/web-app/templates/content/user-login.gohtml b/cmd/web-app/templates/content/user-login.gohtml index f0433f5..3c1bd39 100644 --- a/cmd/web-app/templates/content/user-login.gohtml +++ b/cmd/web-app/templates/content/user-login.gohtml @@ -17,11 +17,12 @@
+ {{ template "app-flashes" . }} +

Welcome Back!

- {{ template "top-error" . }} {{ template "validation-error" . }}
diff --git a/cmd/web-app/templates/content/user-reset-confirm.gohtml b/cmd/web-app/templates/content/user-reset-confirm.gohtml index ef468fe..2da2bc4 100644 --- a/cmd/web-app/templates/content/user-reset-confirm.gohtml +++ b/cmd/web-app/templates/content/user-reset-confirm.gohtml @@ -17,12 +17,13 @@
+ {{ template "app-flashes" . }} +

Reset Your Password

.....

- {{ template "top-error" . }} {{ template "validation-error" . }} diff --git a/cmd/web-app/templates/content/user-reset-password.gohtml b/cmd/web-app/templates/content/user-reset-password.gohtml index f74aae4..f352599 100644 --- a/cmd/web-app/templates/content/user-reset-password.gohtml +++ b/cmd/web-app/templates/content/user-reset-password.gohtml @@ -17,12 +17,13 @@
+ {{ template "app-flashes" . }} +

Forgot Your Password?

We get it, stuff happens. Just enter your email address below and we'll send you a link to reset your password!

- {{ template "top-error" . }} {{ template "validation-error" . }} diff --git a/cmd/web-app/templates/layouts/base.gohtml b/cmd/web-app/templates/layouts/base.gohtml index 63c7077..7b6ebcb 100644 --- a/cmd/web-app/templates/layouts/base.gohtml +++ b/cmd/web-app/templates/layouts/base.gohtml @@ -107,7 +107,26 @@ {{ end }}
{{ end }} -{{ define "top-error" }} +{{ define "app-flashes" }} + {{ if .flashes }} + {{ range $f := .flashes }} + + {{ end }} + {{ end }} {{ if .error }} {{ $errMsg := (ErrorMessage $._Ctx .error) }} {{ $errDetails := (ErrorDetails $._Ctx .error) }} diff --git a/cmd/web-app/templates/partials/page-wrapper.tmpl b/cmd/web-app/templates/partials/page-wrapper.tmpl index db427c6..9b79e53 100644 --- a/cmd/web-app/templates/partials/page-wrapper.tmpl +++ b/cmd/web-app/templates/partials/page-wrapper.tmpl @@ -26,7 +26,7 @@
- {{ template "top-error" . }} + {{ template "app-flashes" . }} {{ template "validation-error" . }} {{ template "content" . }} diff --git a/cmd/web-app/templates/partials/sidebar.tmpl b/cmd/web-app/templates/partials/sidebar.tmpl index 292c28c..c1bc627 100644 --- a/cmd/web-app/templates/partials/sidebar.tmpl +++ b/cmd/web-app/templates/partials/sidebar.tmpl @@ -10,47 +10,73 @@ - - + {{ if HasAuth $._Ctx }} - - + + + + + + + + + + + + + + + + + + {{ end }} - - - diff --git a/cmd/web-app/templates/partials/topbar.tmpl b/cmd/web-app/templates/partials/topbar.tmpl index de4551a..aab71d2 100644 --- a/cmd/web-app/templates/partials/topbar.tmpl +++ b/cmd/web-app/templates/partials/topbar.tmpl @@ -7,178 +7,182 @@ - - -
- -
- + {{ if HasAuth $._Ctx }} + + + +
+ +
+ +
-
- + - -
+ - - +
+
December 7, 2019
+ $290.29 has been deposited into your account! +
+ + +
+
+ +
+
+
+
December 2, 2019
+ Spending Alert: We've noticed unusually high spending for your account. +
+
+ Show All Alerts +
+ - -
- + + + -
+
- -
- - - + + + + + {{ else }} + + {{ end }} {{ end }} \ No newline at end of file diff --git a/internal/platform/web/response.go b/internal/platform/web/response.go index 9a5310f..871c09e 100644 --- a/internal/platform/web/response.go +++ b/internal/platform/web/response.go @@ -4,14 +4,13 @@ import ( "context" "encoding/json" "fmt" - "geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext" - "html/template" "net/http" "os" "path" "path/filepath" "strings" + "geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext" "geeks-accelerator/oss/saas-starter-kit/internal/platform/web/weberror" ) @@ -50,7 +49,7 @@ func RespondJsonError(ctx context.Context, w http.ResponseWriter, er error) erro v.StatusCode = webErr.Status - return RespondJson(ctx, w, webErr.Display(ctx), webErr.Status) + return RespondJson(ctx, w, webErr.Response(ctx, false), webErr.Status) } // RespondJson converts a Go value to JSON and sends it to the client. @@ -120,7 +119,7 @@ func RespondErrorStatus(ctx context.Context, w http.ResponseWriter, er error, st webErr := weberror.NewError(ctx, er, v.StatusCode).(*weberror.Error) v.StatusCode = webErr.Status - respErr := webErr.Display(ctx).String() + respErr := webErr.Response(ctx, false).String() switch webcontext.ContextEnv(ctx) { case webcontext.Env_Dev, webcontext.Env_Stage: @@ -182,30 +181,17 @@ func RenderError(ctx context.Context, w http.ResponseWriter, r *http.Request, er // 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 - - respErr := webErr.Display(ctx) - - var fullError string - switch webcontext.ContextEnv(ctx) { - case webcontext.Env_Dev, webcontext.Env_Stage: - if webErr.Cause != nil && webErr.Cause.Error() != webErr.Err.Error() { - fullError = fmt.Sprintf("\n%s\n%+v", webErr.Error(), webErr.Cause) - } else { - fullError = fmt.Sprintf("%+v", webErr.Err) - } - - fullError = strings.Replace(fullError, "\n", "
", -1) - } + webErr := weberror.NewError(ctx, er, v.StatusCode).(*weberror.Error).Response(ctx, true) + v.StatusCode = webErr.StatusCode data := map[string]interface{}{ - "statusCode": webErr.Status, - "errorMessage": respErr.Error, - "fullError": template.HTML(fullError), + "StatusCode": webErr.StatusCode, + "Error": webErr.Error, + "Details": webErr.Details, + "Fields": webErr.Fields, } - return renderer.Render(ctx, w, r, templateLayoutName, templateContentName, contentType, webErr.Status, data) + return renderer.Render(ctx, w, r, templateLayoutName, templateContentName, contentType, webErr.StatusCode, data) } // Static registers a new route with path prefix to serve static files from the diff --git a/internal/platform/web/template-renderer/template_renderer.go b/internal/platform/web/template-renderer/template_renderer.go index 1b2a00b..dcdc395 100644 --- a/internal/platform/web/template-renderer/template_renderer.go +++ b/internal/platform/web/template-renderer/template_renderer.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "geeks-accelerator/oss/saas-starter-kit/internal/platform/auth" + "geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext" "geeks-accelerator/oss/saas-starter-kit/internal/platform/web/weberror" "html/template" "math" @@ -351,10 +352,23 @@ func (r *TemplateRenderer) Render(ctx context.Context, w http.ResponseWriter, re } } + // If there is a session, check for flashes and ensure the session is saved. + sess := webcontext.ContextSession(ctx) + if sess != nil { + // Load any flash messages and append to response data to be included in the rendered template. + if flashes := sess.Flashes(); len(flashes) > 0 { + renderData["flashes"] = flashes + } + + // Save the session before writing to the response for the session cookie to be sent to the client. + if err := sess.Save(req, w); err != nil { + return err + } + } + // Render template with data. - err := t.Execute(w, renderData) - if err != nil { - return err + if err := t.Execute(w, renderData); err != nil { + return errors.WithStack(err) } return nil diff --git a/internal/platform/web/webcontext/flash_message.go b/internal/platform/web/webcontext/flash_message.go new file mode 100644 index 0000000..43f1a57 --- /dev/null +++ b/internal/platform/web/webcontext/flash_message.go @@ -0,0 +1,81 @@ +package webcontext + +import ( + "context" + "encoding/gob" + "html/template" +) + +type FlashType string + +var ( + FlashType_Success FlashType = "success" + FlashType_Info FlashType = "info" + FlashType_Warning FlashType = "warning" + FlashType_Error FlashType = "danger" +) + +type FlashMsg struct { + Type FlashType `json:"type"` + Title string `json:"title"` + Text string `json:"text"` + Items []string `json:"items"` + Details string `json:"details"` +} + +func (r FlashMsg) Response(ctx context.Context) map[string]interface{} { + var items []template.HTML + for _, i := range r.Items { + items = append(items, template.HTML(i)) + } + + return map[string]interface{}{ + "Type": r.Type, + "Title": r.Title, + "Text": template.HTML(r.Text), + "Items": items, + "Details": template.HTML(r.Details), + } +} + +func init() { + gob.Register(&FlashMsg{}) +} + +// SessionAddFlash loads the session from context that is provided by the session middleware and +// adds the message to the session. The renderer should save the session before writing the response +// to the client or save be directly invoked. +func SessionAddFlash(ctx context.Context, msg FlashMsg) { + ContextSession(ctx).AddFlash(msg.Response(ctx)) +} + +// SessionFlashSuccess add a message with type Success. +func SessionFlashSuccess(ctx context.Context, title, text string, items ...string) { + sessionFlashType(ctx, FlashType_Success, title, text, items...) +} + +// SessionFlashInfo add a message with type Info. +func SessionFlashInfo(ctx context.Context, title, text string, items ...string) { + sessionFlashType(ctx, FlashType_Info, title, text, items...) +} + +// SessionFlashWarning add a message with type Warning. +func SessionFlashWarning(ctx context.Context, title, text string, items ...string) { + sessionFlashType(ctx, FlashType_Warning, title, text, items...) +} + +// SessionFlashError add a message with type Error. +func SessionFlashError(ctx context.Context, title, text string, items ...string) { + sessionFlashType(ctx, FlashType_Error, title, text, items...) +} + +// sessionFlashType adds a flash message with the specified type. +func sessionFlashType(ctx context.Context, flashType FlashType, title, text string, items ...string) { + msg := FlashMsg{ + Type: flashType, + Title: title, + Text: text, + Items: items, + } + SessionAddFlash(ctx, msg) +} diff --git a/internal/platform/web/webcontext/session.go b/internal/platform/web/webcontext/session.go index b3c89ae..a403973 100644 --- a/internal/platform/web/webcontext/session.go +++ b/internal/platform/web/webcontext/session.go @@ -12,6 +12,9 @@ type ctxKeySession int // KeySession is used to store/retrieve a Session from a context.Context. const KeySession ctxKeySession = 1 +// KeyAccessToken is used to store the access token for the user in their session. +const KeyAccessToken = "AccessToken" + // ContextWithSession appends a universal translator to a context. func ContextWithSession(ctx context.Context, session *sessions.Session) context.Context { return context.WithValue(ctx, KeySession, session) @@ -29,7 +32,7 @@ func ContextAccessToken(ctx context.Context) (string, bool) { } func SessionAccessToken(session *sessions.Session) (string, bool) { - if sv, ok := session.Values["AccessToken"].(string); ok { + if sv, ok := session.Values[KeyAccessToken].(string); ok { return sv, true } @@ -39,9 +42,9 @@ func SessionAccessToken(session *sessions.Session) (string, bool) { func SessionWithAccessToken(session *sessions.Session, accessToken string) *sessions.Session { if accessToken != "" { - session.Values["AccessToken"] = accessToken + session.Values[KeyAccessToken] = accessToken } else { - delete(session.Values, "AccessToken") + delete(session.Values, KeyAccessToken) } return session diff --git a/internal/platform/web/webcontext/transvalidate.go b/internal/platform/web/webcontext/transvalidate.go index 7a5de28..adcc4d0 100644 --- a/internal/platform/web/webcontext/transvalidate.go +++ b/internal/platform/web/webcontext/transvalidate.go @@ -72,7 +72,6 @@ func init() { transNl, _ := uniTrans.GetTranslator(nl.Locale()) transZh, _ := uniTrans.GetTranslator(zh.Locale()) - transEn.Add("{{name}}", "Name", false) transFr.Add("{{name}}", "Nom", false) diff --git a/internal/platform/web/weberror/error.go b/internal/platform/web/weberror/error.go index 94b35e0..33bebbd 100644 --- a/internal/platform/web/weberror/error.go +++ b/internal/platform/web/weberror/error.go @@ -2,8 +2,10 @@ package weberror import ( "context" + "fmt" "geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext" "net/http" + "strings" "github.com/pkg/errors" ) @@ -11,11 +13,12 @@ import ( // Error is used to pass an error during the request through the // application with web specific context. type Error struct { - Err error - Status int - Fields []FieldError - Cause error - Message string + Err error + Status int + Fields []FieldError + Cause error + Message string + isValidationError bool } // FieldError is used to indicate an error with a specific request field. @@ -62,7 +65,7 @@ func NewError(ctx context.Context, er error, status int) error { cause = er } - return &Error{er, status, nil, cause, ""} + return &Error{er, status, nil, cause, "", false} } // Error implements the error interface. It uses the default message of the @@ -77,26 +80,45 @@ func (err *Error) Error() string { } // Display renders an error that can be returned as ErrorResponse to the user via the API. -func (er *Error) Display(ctx context.Context) ErrorResponse { +func (er *Error) Response(ctx context.Context, htmlEntities bool) ErrorResponse { var r ErrorResponse if er.Message != "" { r.Error = er.Message } else { - r.Error = er.Error() + r.Error = http.StatusText(er.Status) } if len(er.Fields) > 0 { r.Fields = er.Fields } + switch webcontext.ContextEnv(ctx) { + case webcontext.Env_Dev, webcontext.Env_Stage: + r.Details = fmt.Sprintf("%v", er.Err) + + if er.Cause != nil && er.Cause.Error() != er.Err.Error() { + r.StackTrace = fmt.Sprintf("%+v", er.Cause) + } else { + r.StackTrace = fmt.Sprintf("%+v", er.Err) + } + } + + if htmlEntities { + r.Details = strings.Replace(r.Details, "\n", "
", -1) + r.StackTrace = strings.Replace(r.StackTrace, "\n", "
", -1) + } + return r } // ErrorResponse is the form used for API responses from failures in the API. type ErrorResponse struct { - Error string `json:"error"` - Fields []FieldError `json:"fields,omitempty"` + StatusCode int `json:"status_code"` + Error string `json:"error"` + Details string `json:"details,omitempty"` + StackTrace string `json:"stack_trace,omitempty"` + Fields []FieldError `json:"fields,omitempty"` } // String returns the ErrorResponse formatted as a string. @@ -124,3 +146,31 @@ func WithMessage(ctx context.Context, er error, msg string) error { weberr.Message = msg return weberr } + +// SessionFlashError +func SessionFlashError(ctx context.Context, er error) { + + webErr := NewError(ctx, er, 0).(*Error) + + resp := webErr.Response(ctx, true) + + msg := webcontext.FlashMsg{ + Type: webcontext.FlashType_Error, + Title: resp.Error, + } + + if webErr.isValidationError { + for _, f := range resp.Fields { + msg.Items = append(msg.Items, f.Display) + } + } else { + msg.Text = resp.Details + msg.Details = resp.StackTrace + } + + if pts := strings.Split(msg.Details, "
"); len(pts) > 3 { + msg.Details = strings.Join(pts[0:3], "
") + } + + webcontext.SessionAddFlash(ctx, msg) +} diff --git a/internal/platform/web/weberror/validation.go b/internal/platform/web/weberror/validation.go index 65d3a54..8de603d 100644 --- a/internal/platform/web/weberror/validation.go +++ b/internal/platform/web/weberror/validation.go @@ -78,11 +78,12 @@ func NewValidationError(ctx context.Context, err error) (error, bool) { } return &Error{ - Err: err, - Status: http.StatusBadRequest, - Fields: fields, - Cause: err, - Message: "Field validation error", + Err: err, + Status: http.StatusBadRequest, + Fields: fields, + Cause: err, + Message: "Field validation error", + isValidationError: true, }, true }