You've already forked golang-saas-starter-kit
mirror of
https://github.com/raseels-repos/golang-saas-starter-kit.git
synced 2025-08-08 22:36:41 +02:00
completed flash messages
This commit is contained in:
136
cmd/web-app/handlers/examples.go
Normal file
136
cmd/web-app/handlers/examples.go
Normal file
@ -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)
|
||||
}
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -1,4 +1,4 @@
|
||||
{{define "title"}}Error {{ .statusCode }}{{end}}
|
||||
{{define "title"}}Error {{ .StatusCode }}{{end}}
|
||||
{{define "style"}}
|
||||
|
||||
{{end}}
|
||||
@ -7,10 +7,10 @@
|
||||
|
||||
<!-- Error Text -->
|
||||
<div class="text-center mt-5">
|
||||
<div class="error mx-auto" data-text="{{ .statusCode }}">{{ .statusCode }}</div>
|
||||
<p class="lead text-gray-800 mb-5">{{ .errorMessage }}</p>
|
||||
{{ if .fullError }}
|
||||
<p class="text-gray-500 mb-0">{{ .fullError }}</p>
|
||||
<div class="error mx-auto" data-text="{{ .StatusCode }}">{{ .StatusCode }}</div>
|
||||
<p class="lead text-gray-800 mb-5">{{ .Error }}</p>
|
||||
{{ if .Details }}
|
||||
<p class="text-gray-500 mb-0">{{ .Details }}</p>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
|
25
cmd/web-app/templates/content/examples-flash-messages.gohtml
Normal file
25
cmd/web-app/templates/content/examples-flash-messages.gohtml
Normal file
@ -0,0 +1,25 @@
|
||||
{{define "title"}}Example - Flash Messages{{end}}
|
||||
{{define "style"}}
|
||||
|
||||
{{end}}
|
||||
{{define "content"}}
|
||||
|
||||
<h3>Inline Validation Example</h3>
|
||||
<p>Any field error that is not displayed inline will still be displayed as apart of the the validation at the top of the page.</p>
|
||||
<form class="user" method="post" novalidate>
|
||||
<div class="form-group">
|
||||
<input type="email" class="form-control form-control-user {{ ValidationFieldClass $.validationErrors "Email" }}" name="Email" value="{{ $.form.Email }}" placeholder="Enter Email Address...">
|
||||
{{template "invalid-feedback" dict "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors "fieldName" "Email" }}
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary btn-user btn-block">
|
||||
Submit Form
|
||||
</button>
|
||||
<hr>
|
||||
</form>
|
||||
|
||||
|
||||
{{end}}
|
||||
{{define "js"}}
|
||||
|
||||
{{end}}
|
42
cmd/web-app/templates/content/examples-images.gohtml
Normal file
42
cmd/web-app/templates/content/examples-images.gohtml
Normal file
@ -0,0 +1,42 @@
|
||||
{{define "title"}}Example - Responsive Images{{end}}
|
||||
{{define "style"}}
|
||||
|
||||
{{end}}
|
||||
{{define "content"}}
|
||||
|
||||
<p>S3ImgSrcLarge
|
||||
<img {{ S3ImgSrcLarge $._ctx "/assets/images/glacier-example-pic.jpg" }}/>
|
||||
</p>
|
||||
|
||||
<p>S3ImgThumbSrcLarge
|
||||
<img {{ S3ImgThumbSrcLarge $._ctx "/assets/images/glacier-example-pic.jpg" }}/>
|
||||
</p>
|
||||
|
||||
<p>S3ImgSrcMedium
|
||||
<img {{ S3ImgSrcMedium $._ctx "/assets/images/glacier-example-pic.jpg" }}/>
|
||||
</p>
|
||||
|
||||
<p>S3ImgThumbSrcMedium
|
||||
<img {{ S3ImgThumbSrcMedium $._ctx "/assets/images/glacier-example-pic.jpg" }}/>
|
||||
</p>
|
||||
|
||||
<p>S3ImgSrcSmall
|
||||
<img {{ S3ImgSrcSmall $._ctx "/assets/images/glacier-example-pic.jpg" }}/>
|
||||
</p>
|
||||
|
||||
<p>S3ImgThumbSrcSmall
|
||||
<img {{ S3ImgThumbSrcSmall $._ctx "/assets/images/glacier-example-pic.jpg" }}/>
|
||||
</p>
|
||||
|
||||
<p>S3ImgSrc
|
||||
<img {{ S3ImgSrc $._ctx "/assets/images/glacier-example-pic.jpg" $.imgSizes }}/>
|
||||
</p>
|
||||
|
||||
<p>S3ImgUrl
|
||||
<img src="{{ S3ImgUrl $._ctx "/assets/images/glacier-example-pic.jpg" 200 }}" />
|
||||
</p>
|
||||
|
||||
{{end}}
|
||||
{{define "js"}}
|
||||
|
||||
{{end}}
|
@ -12,11 +12,12 @@
|
||||
<div class="col-lg-5 d-none d-lg-block bg-register-image"></div>
|
||||
<div class="col-lg-7">
|
||||
<div class="p-5">
|
||||
{{ template "app-flashes" . }}
|
||||
|
||||
<div class="text-center">
|
||||
<h1 class="h4 text-gray-900 mb-4">Create an Account!</h1>
|
||||
</div>
|
||||
|
||||
{{ template "top-error" . }}
|
||||
{{ template "validation-error" . }}
|
||||
|
||||
<hr>
|
||||
|
@ -17,11 +17,12 @@
|
||||
<div class="col-lg-6 d-none d-lg-block bg-login-image"></div>
|
||||
<div class="col-lg-6">
|
||||
<div class="p-5">
|
||||
{{ template "app-flashes" . }}
|
||||
|
||||
<div class="text-center">
|
||||
<h1 class="h4 text-gray-900 mb-4">Welcome Back!</h1>
|
||||
</div>
|
||||
|
||||
{{ template "top-error" . }}
|
||||
{{ template "validation-error" . }}
|
||||
|
||||
<form class="user" method="post" novalidate>
|
||||
|
@ -17,12 +17,13 @@
|
||||
<div class="col-lg-6 d-none d-lg-block bg-login-image"></div>
|
||||
<div class="col-lg-6">
|
||||
<div class="p-5">
|
||||
{{ template "app-flashes" . }}
|
||||
|
||||
<div class="text-center">
|
||||
<h1 class="h4 text-gray-900 mb-2">Reset Your Password</h1>
|
||||
<p class="mb-4">.....</p>
|
||||
</div>
|
||||
|
||||
{{ template "top-error" . }}
|
||||
{{ template "validation-error" . }}
|
||||
|
||||
<form class="user" method="post" novalidate>
|
||||
|
@ -17,12 +17,13 @@
|
||||
<div class="col-lg-6 d-none d-lg-block bg-login-image"></div>
|
||||
<div class="col-lg-6">
|
||||
<div class="p-5">
|
||||
{{ template "app-flashes" . }}
|
||||
|
||||
<div class="text-center">
|
||||
<h1 class="h4 text-gray-900 mb-2">Forgot Your Password?</h1>
|
||||
<p class="mb-4">We get it, stuff happens. Just enter your email address below and we'll send you a link to reset your password!</p>
|
||||
</div>
|
||||
|
||||
{{ template "top-error" . }}
|
||||
{{ template "validation-error" . }}
|
||||
|
||||
<form class="user" method="post" novalidate>
|
||||
|
@ -107,7 +107,26 @@
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ define "top-error" }}
|
||||
{{ define "app-flashes" }}
|
||||
{{ if .flashes }}
|
||||
{{ range $f := .flashes }}
|
||||
<div class="alert alert-{{ $f.Type }}" role="alert">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close"> <span aria-hidden="true">×</span> </button>
|
||||
{{ if $f.Title }}<h3>{{ $f.Title }}</h3>{{end}}
|
||||
{{ if $f.Text }}<p>{{ $f.Text }}</p>{{end}}
|
||||
{{ if $f.Items }}
|
||||
<ul>
|
||||
{{ range $i := $f.Items }}
|
||||
<li>{{ $i }}</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
{{ end }}
|
||||
{{ if $f.Details }}
|
||||
<p><small>{{ $f.Details }}</small></p>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ if .error }}
|
||||
{{ $errMsg := (ErrorMessage $._Ctx .error) }}
|
||||
{{ $errDetails := (ErrorDetails $._Ctx .error) }}
|
||||
|
@ -26,7 +26,7 @@
|
||||
<!-- ============================================================== -->
|
||||
<div class="container-fluid" id="page-content">
|
||||
|
||||
{{ template "top-error" . }}
|
||||
{{ template "app-flashes" . }}
|
||||
{{ template "validation-error" . }}
|
||||
|
||||
{{ template "content" . }}
|
||||
|
@ -10,6 +10,8 @@
|
||||
<div class="sidebar-brand-text mx-3">Example Project</div>
|
||||
</a>
|
||||
|
||||
{{ if HasAuth $._Ctx }}
|
||||
|
||||
<!-- Divider -->
|
||||
<hr class="sidebar-divider my-0">
|
||||
|
||||
@ -54,6 +56,30 @@
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{{ end }}
|
||||
|
||||
<!-- Divider -->
|
||||
<hr class="sidebar-divider">
|
||||
|
||||
<!-- Heading -->
|
||||
<div class="sidebar-heading">
|
||||
Examples
|
||||
</div>
|
||||
|
||||
<!-- Nav Item - Pages Collapse Menu -->
|
||||
<li class="nav-item">
|
||||
<a class="nav-link collapsed" href="#" data-toggle="collapse" data-target="#navSectionComponents" aria-expanded="true" aria-controls="navSectionComponents">
|
||||
<i class="fas fa-fw fa-cog"></i>
|
||||
<span>Components</span>
|
||||
</a>
|
||||
<div id="navSectionComponents" class="collapse" aria-labelledby="headingTwo" data-parent="#accordionSidebar">
|
||||
<div class="bg-white py-2 collapse-inner rounded">
|
||||
<h6 class="collapse-header">Custom Components:</h6>
|
||||
<a class="collapse-item" href="/examples/flash-messages">Flash Messages</a>
|
||||
<a class="collapse-item" href="/examples/images">Responsive Images</a>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<!-- Divider -->
|
||||
<hr class="sidebar-divider d-none d-md-block">
|
||||
|
@ -7,6 +7,8 @@
|
||||
<i class="fa fa-bars"></i>
|
||||
</button>
|
||||
|
||||
{{ if HasAuth $._Ctx }}
|
||||
|
||||
<!-- Topbar Search -->
|
||||
<form class="d-none d-sm-inline-block form-inline mr-auto ml-md-3 my-2 my-md-0 mw-100 navbar-search">
|
||||
<div class="input-group">
|
||||
@ -178,7 +180,9 @@
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
{{ else }}
|
||||
<button onclick="window.location='/user/login'; return false;">Login</button>
|
||||
{{ end }}
|
||||
</nav>
|
||||
<!-- End of Topbar -->
|
||||
{{ end }}
|
@ -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", "<br/>", -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
|
||||
|
@ -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,11 +352,24 @@ func (r *TemplateRenderer) Render(ctx context.Context, w http.ResponseWriter, re
|
||||
}
|
||||
}
|
||||
|
||||
// Render template with data.
|
||||
err := t.Execute(w, renderData)
|
||||
if err != nil {
|
||||
// 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.
|
||||
if err := t.Execute(w, renderData); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
81
internal/platform/web/webcontext/flash_message.go
Normal file
81
internal/platform/web/webcontext/flash_message.go
Normal file
@ -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)
|
||||
}
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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"
|
||||
)
|
||||
@ -16,6 +18,7 @@ type Error struct {
|
||||
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,25 +80,44 @@ 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", "<br/>", -1)
|
||||
r.StackTrace = strings.Replace(r.StackTrace, "\n", "<br/>", -1)
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// ErrorResponse is the form used for API responses from failures in the API.
|
||||
type ErrorResponse struct {
|
||||
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"`
|
||||
}
|
||||
|
||||
@ -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, "<br/>"); len(pts) > 3 {
|
||||
msg.Details = strings.Join(pts[0:3], "<br/>")
|
||||
}
|
||||
|
||||
webcontext.SessionAddFlash(ctx, msg)
|
||||
}
|
||||
|
@ -83,6 +83,7 @@ func NewValidationError(ctx context.Context, err error) (error, bool) {
|
||||
Fields: fields,
|
||||
Cause: err,
|
||||
Message: "Field validation error",
|
||||
isValidationError: true,
|
||||
}, true
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user