1
0
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:
Lee Brown
2019-08-03 15:01:17 -08:00
parent 0344473c1e
commit 32cb554dfa
22 changed files with 653 additions and 244 deletions

View 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)
}

View File

@ -3,13 +3,13 @@ package handlers
import ( import (
"context" "context"
"fmt" "fmt"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/notify"
"log" "log"
"net/http" "net/http"
"os" "os"
"geeks-accelerator/oss/saas-starter-kit/internal/mid" "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/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"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext" "geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/weberror" "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("POST", "/signup", s.Step1)
app.Handle("GET", "/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 // Register geo
g := Geo{ g := Geo{
MasterDB: masterDB, MasterDB: masterDB,

View File

@ -79,6 +79,11 @@ func (h *Signup) Step1(ctx context.Context, w http.ResponseWriter, r *http.Reque
return err 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. // Redirect the user to the dashboard.
http.Redirect(w, r, "/", http.StatusFound) http.Redirect(w, r, "/", http.StatusFound)
return nil return nil

View File

@ -2,6 +2,7 @@ package handlers
import ( import (
"context" "context"
"fmt"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/notify" "geeks-accelerator/oss/saas-starter-kit/internal/platform/notify"
project_routes "geeks-accelerator/oss/saas-starter-kit/internal/project-routes" project_routes "geeks-accelerator/oss/saas-starter-kit/internal/project-routes"
"net/http" "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 return nil

View File

@ -1,4 +1,4 @@
{{define "title"}}Error {{ .statusCode }}{{end}} {{define "title"}}Error {{ .StatusCode }}{{end}}
{{define "style"}} {{define "style"}}
{{end}} {{end}}
@ -7,10 +7,10 @@
<!-- Error Text --> <!-- Error Text -->
<div class="text-center mt-5"> <div class="text-center mt-5">
<div class="error mx-auto" data-text="{{ .statusCode }}">{{ .statusCode }}</div> <div class="error mx-auto" data-text="{{ .StatusCode }}">{{ .StatusCode }}</div>
<p class="lead text-gray-800 mb-5">{{ .errorMessage }}</p> <p class="lead text-gray-800 mb-5">{{ .Error }}</p>
{{ if .fullError }} {{ if .Details }}
<p class="text-gray-500 mb-0">{{ .fullError }}</p> <p class="text-gray-500 mb-0">{{ .Details }}</p>
{{ end }} {{ end }}
</div> </div>

View 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}}

View 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}}

View File

@ -12,11 +12,12 @@
<div class="col-lg-5 d-none d-lg-block bg-register-image"></div> <div class="col-lg-5 d-none d-lg-block bg-register-image"></div>
<div class="col-lg-7"> <div class="col-lg-7">
<div class="p-5"> <div class="p-5">
{{ template "app-flashes" . }}
<div class="text-center"> <div class="text-center">
<h1 class="h4 text-gray-900 mb-4">Create an Account!</h1> <h1 class="h4 text-gray-900 mb-4">Create an Account!</h1>
</div> </div>
{{ template "top-error" . }}
{{ template "validation-error" . }} {{ template "validation-error" . }}
<hr> <hr>

View File

@ -17,11 +17,12 @@
<div class="col-lg-6 d-none d-lg-block bg-login-image"></div> <div class="col-lg-6 d-none d-lg-block bg-login-image"></div>
<div class="col-lg-6"> <div class="col-lg-6">
<div class="p-5"> <div class="p-5">
{{ template "app-flashes" . }}
<div class="text-center"> <div class="text-center">
<h1 class="h4 text-gray-900 mb-4">Welcome Back!</h1> <h1 class="h4 text-gray-900 mb-4">Welcome Back!</h1>
</div> </div>
{{ template "top-error" . }}
{{ template "validation-error" . }} {{ template "validation-error" . }}
<form class="user" method="post" novalidate> <form class="user" method="post" novalidate>

View File

@ -17,12 +17,13 @@
<div class="col-lg-6 d-none d-lg-block bg-login-image"></div> <div class="col-lg-6 d-none d-lg-block bg-login-image"></div>
<div class="col-lg-6"> <div class="col-lg-6">
<div class="p-5"> <div class="p-5">
{{ template "app-flashes" . }}
<div class="text-center"> <div class="text-center">
<h1 class="h4 text-gray-900 mb-2">Reset Your Password</h1> <h1 class="h4 text-gray-900 mb-2">Reset Your Password</h1>
<p class="mb-4">.....</p> <p class="mb-4">.....</p>
</div> </div>
{{ template "top-error" . }}
{{ template "validation-error" . }} {{ template "validation-error" . }}
<form class="user" method="post" novalidate> <form class="user" method="post" novalidate>

View File

@ -17,12 +17,13 @@
<div class="col-lg-6 d-none d-lg-block bg-login-image"></div> <div class="col-lg-6 d-none d-lg-block bg-login-image"></div>
<div class="col-lg-6"> <div class="col-lg-6">
<div class="p-5"> <div class="p-5">
{{ template "app-flashes" . }}
<div class="text-center"> <div class="text-center">
<h1 class="h4 text-gray-900 mb-2">Forgot Your Password?</h1> <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> <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> </div>
{{ template "top-error" . }}
{{ template "validation-error" . }} {{ template "validation-error" . }}
<form class="user" method="post" novalidate> <form class="user" method="post" novalidate>

View File

@ -107,7 +107,26 @@
{{ end }} {{ end }}
</div> </div>
{{ end }} {{ 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 }} {{ if .error }}
{{ $errMsg := (ErrorMessage $._Ctx .error) }} {{ $errMsg := (ErrorMessage $._Ctx .error) }}
{{ $errDetails := (ErrorDetails $._Ctx .error) }} {{ $errDetails := (ErrorDetails $._Ctx .error) }}

View File

@ -26,7 +26,7 @@
<!-- ============================================================== --> <!-- ============================================================== -->
<div class="container-fluid" id="page-content"> <div class="container-fluid" id="page-content">
{{ template "top-error" . }} {{ template "app-flashes" . }}
{{ template "validation-error" . }} {{ template "validation-error" . }}
{{ template "content" . }} {{ template "content" . }}

View File

@ -10,47 +10,73 @@
<div class="sidebar-brand-text mx-3">Example Project</div> <div class="sidebar-brand-text mx-3">Example Project</div>
</a> </a>
<!-- Divider --> {{ if HasAuth $._Ctx }}
<hr class="sidebar-divider my-0">
<!-- Nav Item - Dashboard --> <!-- Divider -->
<li class="nav-item"> <hr class="sidebar-divider my-0">
<a class="nav-link" href="/">
<i class="fas fa-fw fa-tachometer-alt"></i> <!-- Nav Item - Dashboard -->
<span>Dashboard</span></a> <li class="nav-item">
</li> <a class="nav-link" href="/">
<i class="fas fa-fw fa-tachometer-alt"></i>
<span>Dashboard</span></a>
</li>
<!-- Divider -->
<hr class="sidebar-divider">
<!-- Heading -->
<div class="sidebar-heading">
Interface
</div>
<!-- Nav Item - Pages Collapse Menu -->
<li class="nav-item">
<a class="nav-link collapsed" href="#" data-toggle="collapse" data-target="#navSectionProjects" aria-expanded="true" aria-controls="navSectionProjects">
<i class="fas fa-fw fa-cog"></i>
<span>Projects</span>
</a>
<div id="navSectionProjects" class="collapse" data-parent="#accordionSidebar">
<div class="bg-white py-2 collapse-inner rounded">
<a class="collapse-item" href="buttons.html">Buttons</a>
<a class="collapse-item" href="cards.html">Cards</a>
</div>
</div>
</li>
<!-- Nav Item - Utilities Collapse Menu -->
<li class="nav-item">
<a class="nav-link collapsed" href="#" data-toggle="collapse" data-target="#navSectionUsers" aria-expanded="true" aria-controls="navSectionUsers">
<i class="fas fa-fw fa-wrench"></i>
<span>Users</span>
</a>
<div id="navSectionUsers" class="collapse" data-parent="#accordionSidebar">
<div class="bg-white py-2 collapse-inner rounded">
<a class="collapse-item" href="/users">Users</a>
</div>
</div>
</li>
{{ end }}
<!-- Divider --> <!-- Divider -->
<hr class="sidebar-divider"> <hr class="sidebar-divider">
<!-- Heading --> <!-- Heading -->
<div class="sidebar-heading"> <div class="sidebar-heading">
Interface Examples
</div> </div>
<!-- Nav Item - Pages Collapse Menu --> <!-- Nav Item - Pages Collapse Menu -->
<li class="nav-item"> <li class="nav-item">
<a class="nav-link collapsed" href="#" data-toggle="collapse" data-target="#navSectionProjects" aria-expanded="true" aria-controls="navSectionProjects"> <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> <i class="fas fa-fw fa-cog"></i>
<span>Projects</span> <span>Components</span>
</a> </a>
<div id="navSectionProjects" class="collapse" data-parent="#accordionSidebar"> <div id="navSectionComponents" class="collapse" aria-labelledby="headingTwo" data-parent="#accordionSidebar">
<div class="bg-white py-2 collapse-inner rounded"> <div class="bg-white py-2 collapse-inner rounded">
<a class="collapse-item" href="buttons.html">Buttons</a> <h6 class="collapse-header">Custom Components:</h6>
<a class="collapse-item" href="cards.html">Cards</a> <a class="collapse-item" href="/examples/flash-messages">Flash Messages</a>
</div> <a class="collapse-item" href="/examples/images">Responsive Images</a>
</div>
</li>
<!-- Nav Item - Utilities Collapse Menu -->
<li class="nav-item">
<a class="nav-link collapsed" href="#" data-toggle="collapse" data-target="#navSectionUsers" aria-expanded="true" aria-controls="navSectionUsers">
<i class="fas fa-fw fa-wrench"></i>
<span>Users</span>
</a>
<div id="navSectionUsers" class="collapse" data-parent="#accordionSidebar">
<div class="bg-white py-2 collapse-inner rounded">
<a class="collapse-item" href="/users">Users</a>
</div> </div>
</div> </div>
</li> </li>

View File

@ -7,178 +7,182 @@
<i class="fa fa-bars"></i> <i class="fa fa-bars"></i>
</button> </button>
<!-- Topbar Search --> {{ if HasAuth $._Ctx }}
<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"> <!-- Topbar Search -->
<input type="text" class="form-control bg-light border-0 small" placeholder="Search for..." aria-label="Search" aria-describedby="basic-addon2"> <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-append"> <div class="input-group">
<button class="btn btn-primary" type="button"> <input type="text" class="form-control bg-light border-0 small" placeholder="Search for..." aria-label="Search" aria-describedby="basic-addon2">
<i class="fas fa-search fa-sm"></i> <div class="input-group-append">
</button> <button class="btn btn-primary" type="button">
<i class="fas fa-search fa-sm"></i>
</button>
</div>
</div> </div>
</div> </form>
</form>
<!-- Topbar Navbar --> <!-- Topbar Navbar -->
<ul class="navbar-nav ml-auto"> <ul class="navbar-nav ml-auto">
<!-- Nav Item - Search Dropdown (Visible Only XS) --> <!-- Nav Item - Search Dropdown (Visible Only XS) -->
<li class="nav-item dropdown no-arrow d-sm-none"> <li class="nav-item dropdown no-arrow d-sm-none">
<a class="nav-link dropdown-toggle" href="#" id="searchDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <a class="nav-link dropdown-toggle" href="#" id="searchDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="fas fa-search fa-fw"></i> <i class="fas fa-search fa-fw"></i>
</a> </a>
<!-- Dropdown - Messages --> <!-- Dropdown - Messages -->
<div class="dropdown-menu dropdown-menu-right p-3 shadow animated--grow-in" aria-labelledby="searchDropdown"> <div class="dropdown-menu dropdown-menu-right p-3 shadow animated--grow-in" aria-labelledby="searchDropdown">
<form class="form-inline mr-auto w-100 navbar-search"> <form class="form-inline mr-auto w-100 navbar-search">
<div class="input-group"> <div class="input-group">
<input type="text" class="form-control bg-light border-0 small" placeholder="Search for..." aria-label="Search" aria-describedby="basic-addon2"> <input type="text" class="form-control bg-light border-0 small" placeholder="Search for..." aria-label="Search" aria-describedby="basic-addon2">
<div class="input-group-append"> <div class="input-group-append">
<button class="btn btn-primary" type="button"> <button class="btn btn-primary" type="button">
<i class="fas fa-search fa-sm"></i> <i class="fas fa-search fa-sm"></i>
</button> </button>
</div>
</div> </div>
</div> </form>
</form> </div>
</div> </li>
</li>
<!-- Nav Item - Alerts --> <!-- Nav Item - Alerts -->
<li class="nav-item dropdown no-arrow mx-1"> <li class="nav-item dropdown no-arrow mx-1">
<a class="nav-link dropdown-toggle" href="#" id="alertsDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <a class="nav-link dropdown-toggle" href="#" id="alertsDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="fas fa-bell fa-fw"></i> <i class="fas fa-bell fa-fw"></i>
<!-- Counter - Alerts --> <!-- Counter - Alerts -->
<span class="badge badge-danger badge-counter">3+</span> <span class="badge badge-danger badge-counter">3+</span>
</a> </a>
<!-- Dropdown - Alerts --> <!-- Dropdown - Alerts -->
<div class="dropdown-list dropdown-menu dropdown-menu-right shadow animated--grow-in" aria-labelledby="alertsDropdown"> <div class="dropdown-list dropdown-menu dropdown-menu-right shadow animated--grow-in" aria-labelledby="alertsDropdown">
<h6 class="dropdown-header"> <h6 class="dropdown-header">
Alerts Center Alerts Center
</h6> </h6>
<a class="dropdown-item d-flex align-items-center" href="#"> <a class="dropdown-item d-flex align-items-center" href="#">
<div class="mr-3"> <div class="mr-3">
<div class="icon-circle bg-primary"> <div class="icon-circle bg-primary">
<i class="fas fa-file-alt text-white"></i> <i class="fas fa-file-alt text-white"></i>
</div>
</div> </div>
</div> <div>
<div> <div class="small text-gray-500">December 12, 2019</div>
<div class="small text-gray-500">December 12, 2019</div> <span class="font-weight-bold">A new monthly report is ready to download!</span>
<span class="font-weight-bold">A new monthly report is ready to download!</span>
</div>
</a>
<a class="dropdown-item d-flex align-items-center" href="#">
<div class="mr-3">
<div class="icon-circle bg-success">
<i class="fas fa-donate text-white"></i>
</div> </div>
</div> </a>
<div> <a class="dropdown-item d-flex align-items-center" href="#">
<div class="small text-gray-500">December 7, 2019</div> <div class="mr-3">
$290.29 has been deposited into your account! <div class="icon-circle bg-success">
</div> <i class="fas fa-donate text-white"></i>
</a> </div>
<a class="dropdown-item d-flex align-items-center" href="#">
<div class="mr-3">
<div class="icon-circle bg-warning">
<i class="fas fa-exclamation-triangle text-white"></i>
</div> </div>
</div> <div>
<div> <div class="small text-gray-500">December 7, 2019</div>
<div class="small text-gray-500">December 2, 2019</div> $290.29 has been deposited into your account!
Spending Alert: We've noticed unusually high spending for your account. </div>
</div> </a>
</a> <a class="dropdown-item d-flex align-items-center" href="#">
<a class="dropdown-item text-center small text-gray-500" href="#">Show All Alerts</a> <div class="mr-3">
</div> <div class="icon-circle bg-warning">
</li> <i class="fas fa-exclamation-triangle text-white"></i>
</div>
</div>
<div>
<div class="small text-gray-500">December 2, 2019</div>
Spending Alert: We've noticed unusually high spending for your account.
</div>
</a>
<a class="dropdown-item text-center small text-gray-500" href="#">Show All Alerts</a>
</div>
</li>
<!-- Nav Item - Messages --> <!-- Nav Item - Messages -->
<li class="nav-item dropdown no-arrow mx-1"> <li class="nav-item dropdown no-arrow mx-1">
<a class="nav-link dropdown-toggle" href="#" id="messagesDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <a class="nav-link dropdown-toggle" href="#" id="messagesDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="fas fa-envelope fa-fw"></i> <i class="fas fa-envelope fa-fw"></i>
<!-- Counter - Messages --> <!-- Counter - Messages -->
<span class="badge badge-danger badge-counter">7</span> <span class="badge badge-danger badge-counter">7</span>
</a>
<!-- Dropdown - Messages -->
<div class="dropdown-list dropdown-menu dropdown-menu-right shadow animated--grow-in" aria-labelledby="messagesDropdown">
<h6 class="dropdown-header">
Message Center
</h6>
<a class="dropdown-item d-flex align-items-center" href="#">
<div class="dropdown-list-image mr-3">
<img class="rounded-circle" src="https://source.unsplash.com/fn_BT9fwg_E/60x60" alt="">
<div class="status-indicator bg-success"></div>
</div>
<div class="font-weight-bold">
<div class="text-truncate">Hi there! I am wondering if you can help me with a problem I've been having.</div>
<div class="small text-gray-500">Emily Fowler · 58m</div>
</div>
</a> </a>
<a class="dropdown-item d-flex align-items-center" href="#"> <!-- Dropdown - Messages -->
<div class="dropdown-list-image mr-3"> <div class="dropdown-list dropdown-menu dropdown-menu-right shadow animated--grow-in" aria-labelledby="messagesDropdown">
<img class="rounded-circle" src="https://source.unsplash.com/AU4VPcFN4LE/60x60" alt=""> <h6 class="dropdown-header">
<div class="status-indicator"></div> Message Center
</div> </h6>
<div> <a class="dropdown-item d-flex align-items-center" href="#">
<div class="text-truncate">I have the photos that you ordered last month, how would you like them sent to you?</div> <div class="dropdown-list-image mr-3">
<div class="small text-gray-500">Jae Chun · 1d</div> <img class="rounded-circle" src="https://source.unsplash.com/fn_BT9fwg_E/60x60" alt="">
</div> <div class="status-indicator bg-success"></div>
</a> </div>
<a class="dropdown-item d-flex align-items-center" href="#"> <div class="font-weight-bold">
<div class="dropdown-list-image mr-3"> <div class="text-truncate">Hi there! I am wondering if you can help me with a problem I've been having.</div>
<img class="rounded-circle" src="https://source.unsplash.com/CS2uCrpNzJY/60x60" alt=""> <div class="small text-gray-500">Emily Fowler · 58m</div>
<div class="status-indicator bg-warning"></div> </div>
</div> </a>
<div> <a class="dropdown-item d-flex align-items-center" href="#">
<div class="text-truncate">Last month's report looks great, I am very happy with the progress so far, keep up the good work!</div> <div class="dropdown-list-image mr-3">
<div class="small text-gray-500">Morgan Alvarez · 2d</div> <img class="rounded-circle" src="https://source.unsplash.com/AU4VPcFN4LE/60x60" alt="">
</div> <div class="status-indicator"></div>
</a> </div>
<a class="dropdown-item d-flex align-items-center" href="#"> <div>
<div class="dropdown-list-image mr-3"> <div class="text-truncate">I have the photos that you ordered last month, how would you like them sent to you?</div>
<img class="rounded-circle" src="https://source.unsplash.com/Mv9hjnEUHR4/60x60" alt=""> <div class="small text-gray-500">Jae Chun · 1d</div>
<div class="status-indicator bg-success"></div> </div>
</div> </a>
<div> <a class="dropdown-item d-flex align-items-center" href="#">
<div class="text-truncate">Am I a good boy? The reason I ask is because someone told me that people say this to all dogs, even if they aren't good...</div> <div class="dropdown-list-image mr-3">
<div class="small text-gray-500">Chicken the Dog · 2w</div> <img class="rounded-circle" src="https://source.unsplash.com/CS2uCrpNzJY/60x60" alt="">
</div> <div class="status-indicator bg-warning"></div>
</a> </div>
<a class="dropdown-item text-center small text-gray-500" href="#">Read More Messages</a> <div>
</div> <div class="text-truncate">Last month's report looks great, I am very happy with the progress so far, keep up the good work!</div>
</li> <div class="small text-gray-500">Morgan Alvarez · 2d</div>
</div>
</a>
<a class="dropdown-item d-flex align-items-center" href="#">
<div class="dropdown-list-image mr-3">
<img class="rounded-circle" src="https://source.unsplash.com/Mv9hjnEUHR4/60x60" alt="">
<div class="status-indicator bg-success"></div>
</div>
<div>
<div class="text-truncate">Am I a good boy? The reason I ask is because someone told me that people say this to all dogs, even if they aren't good...</div>
<div class="small text-gray-500">Chicken the Dog · 2w</div>
</div>
</a>
<a class="dropdown-item text-center small text-gray-500" href="#">Read More Messages</a>
</div>
</li>
<div class="topbar-divider d-none d-sm-block"></div> <div class="topbar-divider d-none d-sm-block"></div>
<!-- Nav Item - User Information --> <!-- Nav Item - User Information -->
<li class="nav-item dropdown no-arrow"> <li class="nav-item dropdown no-arrow">
<a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="mr-2 d-none d-lg-inline text-gray-600 small">Valerie Luna</span> <span class="mr-2 d-none d-lg-inline text-gray-600 small">Valerie Luna</span>
<img class="img-profile rounded-circle" src="https://source.unsplash.com/QAB-WJcbgJk/60x60"> <img class="img-profile rounded-circle" src="https://source.unsplash.com/QAB-WJcbgJk/60x60">
</a>
<!-- Dropdown - User Information -->
<div class="dropdown-menu dropdown-menu-right shadow animated--grow-in" aria-labelledby="userDropdown">
<a class="dropdown-item" href="#">
<i class="fas fa-user fa-sm fa-fw mr-2 text-gray-400"></i>
Profile
</a> </a>
<a class="dropdown-item" href="#"> <!-- Dropdown - User Information -->
<i class="fas fa-cogs fa-sm fa-fw mr-2 text-gray-400"></i> <div class="dropdown-menu dropdown-menu-right shadow animated--grow-in" aria-labelledby="userDropdown">
Settings <a class="dropdown-item" href="#">
</a> <i class="fas fa-user fa-sm fa-fw mr-2 text-gray-400"></i>
<a class="dropdown-item" href="#"> Profile
<i class="fas fa-list fa-sm fa-fw mr-2 text-gray-400"></i> </a>
Activity Log <a class="dropdown-item" href="#">
</a> <i class="fas fa-cogs fa-sm fa-fw mr-2 text-gray-400"></i>
<div class="dropdown-divider"></div> Settings
<a class="dropdown-item" href="#" data-toggle="modal" data-target="#logoutModal"> </a>
<i class="fas fa-sign-out-alt fa-sm fa-fw mr-2 text-gray-400"></i> <a class="dropdown-item" href="#">
Logout <i class="fas fa-list fa-sm fa-fw mr-2 text-gray-400"></i>
</a> Activity Log
</div> </a>
</li> <div class="dropdown-divider"></div>
<a class="dropdown-item" href="#" data-toggle="modal" data-target="#logoutModal">
</ul> <i class="fas fa-sign-out-alt fa-sm fa-fw mr-2 text-gray-400"></i>
Logout
</a>
</div>
</li>
</ul>
{{ else }}
<button onclick="window.location='/user/login'; return false;">Login</button>
{{ end }}
</nav> </nav>
<!-- End of Topbar --> <!-- End of Topbar -->
{{ end }} {{ end }}

View File

@ -4,14 +4,13 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext"
"html/template"
"net/http" "net/http"
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
"strings" "strings"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/weberror" "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 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. // 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) webErr := weberror.NewError(ctx, er, v.StatusCode).(*weberror.Error)
v.StatusCode = webErr.Status v.StatusCode = webErr.Status
respErr := webErr.Display(ctx).String() respErr := webErr.Response(ctx, false).String()
switch webcontext.ContextEnv(ctx) { switch webcontext.ContextEnv(ctx) {
case webcontext.Env_Dev, webcontext.Env_Stage: 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 // If the error was of the type *Error, the handler has
// a specific status code and error to return. // a specific status code and error to return.
webErr := weberror.NewError(ctx, er, v.StatusCode).(*weberror.Error) webErr := weberror.NewError(ctx, er, v.StatusCode).(*weberror.Error).Response(ctx, true)
v.StatusCode = webErr.Status v.StatusCode = webErr.StatusCode
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)
}
data := map[string]interface{}{ data := map[string]interface{}{
"statusCode": webErr.Status, "StatusCode": webErr.StatusCode,
"errorMessage": respErr.Error, "Error": webErr.Error,
"fullError": template.HTML(fullError), "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 // Static registers a new route with path prefix to serve static files from the

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/auth" "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" "geeks-accelerator/oss/saas-starter-kit/internal/platform/web/weberror"
"html/template" "html/template"
"math" "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. // Render template with data.
err := t.Execute(w, renderData) if err := t.Execute(w, renderData); err != nil {
if err != nil { return errors.WithStack(err)
return err
} }
return nil return nil

View 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)
}

View File

@ -12,6 +12,9 @@ type ctxKeySession int
// KeySession is used to store/retrieve a Session from a context.Context. // KeySession is used to store/retrieve a Session from a context.Context.
const KeySession ctxKeySession = 1 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. // ContextWithSession appends a universal translator to a context.
func ContextWithSession(ctx context.Context, session *sessions.Session) context.Context { func ContextWithSession(ctx context.Context, session *sessions.Session) context.Context {
return context.WithValue(ctx, KeySession, session) return context.WithValue(ctx, KeySession, session)
@ -29,7 +32,7 @@ func ContextAccessToken(ctx context.Context) (string, bool) {
} }
func SessionAccessToken(session *sessions.Session) (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 return sv, true
} }
@ -39,9 +42,9 @@ func SessionAccessToken(session *sessions.Session) (string, bool) {
func SessionWithAccessToken(session *sessions.Session, accessToken string) *sessions.Session { func SessionWithAccessToken(session *sessions.Session, accessToken string) *sessions.Session {
if accessToken != "" { if accessToken != "" {
session.Values["AccessToken"] = accessToken session.Values[KeyAccessToken] = accessToken
} else { } else {
delete(session.Values, "AccessToken") delete(session.Values, KeyAccessToken)
} }
return session return session

View File

@ -72,7 +72,6 @@ func init() {
transNl, _ := uniTrans.GetTranslator(nl.Locale()) transNl, _ := uniTrans.GetTranslator(nl.Locale())
transZh, _ := uniTrans.GetTranslator(zh.Locale()) transZh, _ := uniTrans.GetTranslator(zh.Locale())
transEn.Add("{{name}}", "Name", false) transEn.Add("{{name}}", "Name", false)
transFr.Add("{{name}}", "Nom", false) transFr.Add("{{name}}", "Nom", false)

View File

@ -2,8 +2,10 @@ package weberror
import ( import (
"context" "context"
"fmt"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext" "geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext"
"net/http" "net/http"
"strings"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
@ -11,11 +13,12 @@ import (
// Error is used to pass an error during the request through the // Error is used to pass an error during the request through the
// application with web specific context. // application with web specific context.
type Error struct { type Error struct {
Err error Err error
Status int Status int
Fields []FieldError Fields []FieldError
Cause error Cause error
Message string Message string
isValidationError bool
} }
// FieldError is used to indicate an error with a specific request field. // 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 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 // 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. // 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 var r ErrorResponse
if er.Message != "" { if er.Message != "" {
r.Error = er.Message r.Error = er.Message
} else { } else {
r.Error = er.Error() r.Error = http.StatusText(er.Status)
} }
if len(er.Fields) > 0 { if len(er.Fields) > 0 {
r.Fields = er.Fields 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 return r
} }
// ErrorResponse is the form used for API responses from failures in the API. // ErrorResponse is the form used for API responses from failures in the API.
type ErrorResponse struct { type ErrorResponse struct {
Error string `json:"error"` StatusCode int `json:"status_code"`
Fields []FieldError `json:"fields,omitempty"` 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. // 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 weberr.Message = msg
return weberr 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)
}

View File

@ -78,11 +78,12 @@ func NewValidationError(ctx context.Context, err error) (error, bool) {
} }
return &Error{ return &Error{
Err: err, Err: err,
Status: http.StatusBadRequest, Status: http.StatusBadRequest,
Fields: fields, Fields: fields,
Cause: err, Cause: err,
Message: "Field validation error", Message: "Field validation error",
isValidationError: true,
}, true }, true
} }