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 (
|
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,
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
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-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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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) }}
|
||||||
|
@ -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" . }}
|
||||||
|
@ -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>
|
||||||
|
@ -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 }}
|
@ -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
|
||||||
|
@ -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
|
||||||
|
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.
|
// 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
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user