Commit c625ace8 authored by Lee Brown's avatar Lee Brown

Completed implimentation of forgot password

parent 1d69ea88
......@@ -2,3 +2,4 @@
aws.lee
aws.*
.env_docker_compose
local.env
......@@ -92,6 +92,7 @@ webapp:deploy:dev:
STATIC_FILES_S3: 'true'
STATIC_FILES_IMG_RESIZE: 'true'
AWS_USE_ROLE: 'true'
EMAIL_SENDER: '[email protected]'
webapi:build:dev:
<<: *build_tmpl
......@@ -131,6 +132,7 @@ webapi:deploy:dev:
STATIC_FILES_S3: 'false'
STATIC_FILES_IMG_RESIZE: 'false'
AWS_USE_ROLE: 'true'
EMAIL_SENDER: '[email protected]'
#ddlogscollector:deploy:stage:
# <<: *deploy_stage_tmpl
......
......@@ -381,7 +381,7 @@ schema migrations before running any unit tests.
To login to the local Postgres container, use the following command:
```bash
docker exec -it saas-starter-kit_postgres_1 /bin/bash
bash-4.4# psql -U postgres shared
bash-5.0# psql -U postgres shared
```
The example project currently only includes a few tables. As more functionality is built into both the web-app and
......
......@@ -37,6 +37,9 @@ COPY cmd/web-api ./cmd/web-api
COPY cmd/web-api/templates /templates
#COPY cmd/web-api/static /static
# Copy the global templates.
ADD resources/templates/shared /templates/shared
WORKDIR ./cmd/web-api
# Update the API documentation.
......@@ -54,6 +57,8 @@ COPY --from=builder /gosrv /
COPY --from=builder /templates /templates
ENV TEMPLATE_DIR=/templates
ENV SHARED_TEMPLATE_DIR=/templates/shared
#ENV STATIC_DIR=/static
ARG service
ENV SERVICE_NAME $service
......
......@@ -38,6 +38,7 @@
{"name": "WEB_API_SERVICE_BASE_URL", "value": "{APP_BASE_URL}"},
{"name": "WEB_API_SERVICE_HOST_NAMES", "value": "{HOST_NAMES}"},
{"name": "WEB_API_SERVICE_ENABLE_HTTPS", "value": "{HTTPS_ENABLED}"},
{"name": "WEB_API_EMAIL_SENDER", "value": "{EMAIL_SENDER}"},
{"name": "WEB_API_REDIS_HOST", "value": "{CACHE_HOST}"},
{"name": "WEB_API_DB_HOST", "value": "{DB_HOST}"},
{"name": "WEB_API_DB_USER", "value": "{DB_USER}"},
......
......@@ -2,3 +2,4 @@ export WEB_API_DB_HOST=127.0.0.1:5433
export WEB_API_DB_USER=postgres
export WEB_API_DB_PASS=postgres
export WEB_API_DB_DISABLE_TLS=true
export [email protected]
......@@ -24,6 +24,9 @@ COPY cmd/web-app ./cmd/web-app
COPY cmd/web-app/templates /templates
COPY cmd/web-app/static /static
# Copy the global templates.
ADD resources/templates/shared /templates/shared
WORKDIR ./cmd/web-app
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix nocgo -o /gosrv .
......@@ -38,6 +41,7 @@ COPY --from=builder /static /static
COPY --from=builder /templates /templates
ENV TEMPLATE_DIR=/templates
ENV SHARED_TEMPLATE_DIR=/templates/shared
ENV STATIC_DIR=/static
ARG service
......
......@@ -42,6 +42,7 @@
{"name": "WEB_APP_SERVICE_STATICFILES_S3_PREFIX", "value": "{STATIC_FILES_S3_PREFIX}"},
{"name": "WEB_APP_SERVICE_STATICFILES_CLOUDFRONT_ENABLED", "value": "{STATIC_FILES_CLOUDFRONT_ENABLED}"},
{"name": "WEB_APP_SERVICE_STATICFILES_IMG_RESIZE_ENABLED", "value": "{STATIC_FILES_IMG_RESIZE_ENABLED}"},
{"name": "WEB_APP_EMAIL_SENDER", "value": "{EMAIL_SENDER}"},
{"name": "WEB_APP_REDIS_HOST", "value": "{CACHE_HOST}"},
{"name": "WEB_APP_DB_HOST", "value": "{DB_HOST}"},
{"name": "WEB_APP_DB_USER", "value": "{DB_USER}"},
......
......@@ -3,6 +3,7 @@ package handlers
import (
"context"
"fmt"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/notify"
"log"
"net/http"
"os"
......@@ -23,7 +24,7 @@ const (
)
// API returns a handler for a set of routes.
func APP(shutdown chan os.Signal, log *log.Logger, env webcontext.Env, staticDir, templateDir string, masterDB *sqlx.DB, redis *redis.Client, authenticator *auth.Authenticator, projectRoutes project_routes.ProjectRoutes, renderer web.Renderer, globalMids ...web.Middleware) http.Handler {
func APP(shutdown chan os.Signal, log *log.Logger, env webcontext.Env, staticDir, templateDir string, masterDB *sqlx.DB, redis *redis.Client, authenticator *auth.Authenticator, projectRoutes project_routes.ProjectRoutes, secretKey string, notifyEmail notify.Email, renderer web.Renderer, globalMids ...web.Middleware) http.Handler {
// Define base middlewares applied to all requests.
middlewares := []web.Middleware{
......@@ -50,13 +51,18 @@ func APP(shutdown chan os.Signal, log *log.Logger, env webcontext.Env, staticDir
MasterDB: masterDB,
Renderer: renderer,
Authenticator: authenticator,
ProjectRoutes: projectRoutes,
NotifyEmail: notifyEmail,
SecretKey: secretKey,
}
// This route is not authenticated
app.Handle("POST", "/user/login", u.Login)
app.Handle("GET", "/user/login", u.Login)
app.Handle("GET", "/user/logout", u.Logout)
app.Handle("POST", "/user/forgot-password", u.ForgotPassword)
app.Handle("GET", "/user/forgot-password", u.ForgotPassword)
app.Handle("POST", "/user/reset-password/:hash", u.ResetConfirm)
app.Handle("GET", "/user/reset-password/:hash", u.ResetConfirm)
app.Handle("POST", "/user/reset-password", u.ResetPassword)
app.Handle("GET", "/user/reset-password", u.ResetPassword)
// Register user management and authentication endpoints.
s := Signup{
......
......@@ -2,6 +2,8 @@ package handlers
import (
"context"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/notify"
project_routes "geeks-accelerator/oss/saas-starter-kit/internal/project-routes"
"net/http"
"time"
......@@ -23,6 +25,9 @@ type User struct {
MasterDB *sqlx.DB
Renderer web.Renderer
Authenticator *auth.Authenticator
ProjectRoutes project_routes.ProjectRoutes
NotifyEmail notify.Email
SecretKey string
}
type UserLoginRequest struct {
......@@ -156,7 +161,156 @@ func (h *User) Logout(ctx context.Context, w http.ResponseWriter, r *http.Reques
}
// List returns all the existing users in the system.
func (h *User) ForgotPassword(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
func (h *User) ResetPassword(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
return h.Renderer.Render(ctx, w, r, tmplLayoutBase, "user-forgot-password.tmpl", web.MIMETextHTMLCharsetUTF8, http.StatusOK, nil)
ctxValues, err := webcontext.ContextValues(ctx)
if err != nil {
return err
}
//
req := new(user.UserResetPasswordRequest)
data := make(map[string]interface{})
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
}
}
_, err = user.ResetPassword(ctx, h.MasterDB, h.ProjectRoutes.UserResetPassword, h.NotifyEmail, *req, h.SecretKey, ctxValues.Now)
if err != nil {
switch errors.Cause(err) {
default:
if verr, ok := weberror.NewValidationError(ctx, err); ok {
data["validationErrors"] = verr.(*weberror.Error)
return nil
} else {
return err
}
}
}
// Display a flash message!!!
}
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(user.UserResetPasswordRequest{})); ok {
data["validationDefaults"] = verr.(*weberror.Error)
}
return h.Renderer.Render(ctx, w, r, tmplLayoutBase, "user-reset-password.gohtml", web.MIMETextHTMLCharsetUTF8, http.StatusOK, data)
}
// List returns all the existing users in the system.
func (h *User) ResetConfirm(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
ctxValues, err := webcontext.ContextValues(ctx)
if err != nil {
return err
}
//
req := new(user.UserResetConfirmRequest)
data := make(map[string]interface{})
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
}
}
u, err := user.ResetConfirm(ctx, h.MasterDB, *req, h.SecretKey, ctxValues.Now)
if err != nil {
switch errors.Cause(err) {
default:
if verr, ok := weberror.NewValidationError(ctx, err); ok {
data["validationErrors"] = verr.(*weberror.Error)
return nil
} else {
return err
}
}
}
// Authenticated the user. Probably should use the default session TTL from UserLogin.
token, err := user.Authenticate(ctx, h.MasterDB, h.Authenticator, u.Email, req.Password, time.Hour, ctxValues.Now)
if err != nil {
switch errors.Cause(err) {
case account.ErrForbidden:
return web.RespondError(ctx, w, weberror.NewError(ctx, err, http.StatusForbidden))
default:
if verr, ok := weberror.NewValidationError(ctx, err); ok {
data["validationErrors"] = verr.(*weberror.Error)
return nil
} else {
return err
}
}
}
// Add the token to the users session.
err = handleSessionToken(ctx, w, r, token)
if err != nil {
return err
}
// Redirect the user to the dashboard.
http.Redirect(w, r, "/", http.StatusFound)
} else {
req.ResetHash = params["hash"]
}
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(user.UserResetConfirmRequest{})); ok {
data["validationDefaults"] = verr.(*weberror.Error)
}
return h.Renderer.Render(ctx, w, r, tmplLayoutBase, "user-reset-confirm.gohtml", web.MIMETextHTMLCharsetUTF8, http.StatusOK, data)
}
......@@ -6,6 +6,8 @@ import (
"encoding/json"
"expvar"
"fmt"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/notify"
"gopkg.in/gomail.v2"
"html/template"
"log"
"net"
......@@ -81,13 +83,14 @@ func main() {
DisableHTTP2 bool `default:"false" envconfig:"DISABLE_HTTP2"`
}
Service struct {
Name string `default:"web-app" envconfig:"NAME"`
Project string `default:"" envconfig:"PROJECT"`
BaseUrl string `default:"" envconfig:"BASE_URL" example:"http://eproc.tech"`
HostNames []string `envconfig:"HOST_NAMES" example:"www.eproc.tech"`
EnableHTTPS bool `default:"false" envconfig:"ENABLE_HTTPS"`
TemplateDir string `default:"./templates" envconfig:"TEMPLATE_DIR"`
StaticFiles struct {
Name string `default:"web-app" envconfig:"NAME"`
Project string `default:"" envconfig:"PROJECT"`
BaseUrl string `default:"" envconfig:"BASE_URL" example:"http://eproc.tech"`
HostNames []string `envconfig:"HOST_NAMES" example:"www.eproc.tech"`
EnableHTTPS bool `default:"false" envconfig:"ENABLE_HTTPS"`
TemplateDir string `default:"./templates" envconfig:"TEMPLATE_DIR"`
SharedTemplateDir string `default:"../../resources/templates/shared" envconfig:"SHARED_TEMPLATE_DIR"`
StaticFiles struct {
Dir string `default:"./static" envconfig:"STATIC_DIR"`
S3Enabled bool `envconfig:"S3_ENABLED"`
S3Prefix string `default:"public/web_app/static" envconfig:"S3_PREFIX"`
......@@ -97,6 +100,7 @@ func main() {
WebApiBaseUrl string `default:"http://127.0.0.1:3001" envconfig:"WEB_API_BASE_URL" example:"http://api.eproc.tech"`
SessionKey string `default:"" envconfig:"SESSION_KEY"`
SessionName string `default:"" envconfig:"SESSION_NAME"`
EmailSender string `default:"" envconfig:"EMAIL_SENDER"`
DebugHost string `default:"0.0.0.0:4000" envconfig:"DEBUG_HOST"`
ShutdownTimeout time.Duration `default:"5s" envconfig:"SHUTDOWN_TIMEOUT"`
}
......@@ -137,6 +141,12 @@ func main() {
UseAwsSecretManager bool `default:"false" envconfig:"USE_AWS_SECRET_MANAGER"`
KeyExpiration time.Duration `default:"3600s" envconfig:"KEY_EXPIRATION"`
}
STMP struct {
Host string `default:"localhost" envconfig:"HOST"`
Port int `default:"25" envconfig:"PORT"`
User string `default:"" envconfig:"USER"`
Pass string `default:"" envconfig:"PASS" json:"-"` // don't print
}
BuildInfo struct {
CiCommitRefName string `envconfig:"CI_COMMIT_REF_NAME"`
CiCommitRefSlug string `envconfig:"CI_COMMIT_REF_SLUG"`
......@@ -351,6 +361,38 @@ func main() {
}
defer masterDb.Close()
// =========================================================================
// Notify Email
var notifyEmail notify.Email
if awsSession != nil {
notifyEmail, err = notify.NewEmailAws(awsSession, cfg.Service.SharedTemplateDir, cfg.Service.EmailSender)
if err != nil {
log.Fatalf("main : Notify Email : %+v", err)
}
err = notifyEmail.Verify()
if err != nil {
switch errors.Cause(err) {
case notify.ErrAwsSesIdentityNotVerified:
log.Printf("main : Notify Email : %s\n", err)
case notify.ErrAwsSesSendingDisabled:
log.Printf("main : Notify Email : %s\n", err)
default:
log.Fatalf("main : Notify Email Verify : %+v", err)
}
}
} else {
d := gomail.Dialer{
Host: cfg.STMP.Host,
Port: cfg.STMP.Port,
Username: cfg.STMP.User,
Password: cfg.STMP.Pass}
notifyEmail, err = notify.NewEmailSmtp(d, cfg.Service.SharedTemplateDir, cfg.Service.EmailSender)
if err != nil {
log.Fatalf("main : Notify Email : %+v", err)
}
}
// =========================================================================
// Init new Authenticator
var authenticator *auth.Authenticator
......@@ -776,7 +818,7 @@ func main() {
if cfg.HTTP.Host != "" {
api := http.Server{
Addr: cfg.HTTP.Host,
Handler: handlers.APP(shutdown, log, cfg.Env, cfg.Service.StaticFiles.Dir, cfg.Service.TemplateDir, masterDb, redisClient, authenticator, projectRoutes, renderer, serviceMiddlewares...),
Handler: handlers.APP(shutdown, log, cfg.Env, cfg.Service.StaticFiles.Dir, cfg.Service.TemplateDir, masterDb, redisClient, authenticator, projectRoutes, cfg.Service.SessionKey, notifyEmail, renderer, serviceMiddlewares...),
ReadTimeout: cfg.HTTP.ReadTimeout,
WriteTimeout: cfg.HTTP.WriteTimeout,
MaxHeaderBytes: 1 << 20,
......@@ -793,7 +835,7 @@ func main() {
if cfg.HTTPS.Host != "" {
api := http.Server{
Addr: cfg.HTTPS.Host,
Handler: handlers.APP(shutdown, log, cfg.Env, cfg.Service.StaticFiles.Dir, cfg.Service.TemplateDir, masterDb, redisClient, authenticator, projectRoutes, renderer, serviceMiddlewares...),
Handler: handlers.APP(shutdown, log, cfg.Env, cfg.Service.StaticFiles.Dir, cfg.Service.TemplateDir, masterDb, redisClient, authenticator, projectRoutes, cfg.Service.SessionKey, notifyEmail, renderer, serviceMiddlewares...),
ReadTimeout: cfg.HTTPS.ReadTimeout,
WriteTimeout: cfg.HTTPS.WriteTimeout,
MaxHeaderBytes: 1 << 20,
......
......@@ -2,3 +2,4 @@ export WEB_APP_DB_HOST=127.0.0.1:5433
export WEB_APP_DB_USER=postgres
export WEB_APP_DB_PASS=postgres
export WEB_APP_DB_DISABLE_TLS=true
export [email protected]
\ No newline at end of file
console.log("test");
\ No newline at end of file
$(document).ready(function() {
// Prevent duplicate validation messages. When the validation error is displayed inline
// when the form value, don't display the form error message at the top of the page.
$(this).find('#page-content form').find('input, select, textarea').each(function(index){
var fname = $(this).attr('name');
if (fname === undefined) {
return;
}
var vnode = $(this).parent().find('div.invalid-feedback');
var formField = $(vnode).attr('data-field');
$(document).find('div.validation-error').find('li').each(function(){
if ($(this).attr('data-form-field') == formField) {
if ($(vnode).is(":visible")) {
$(this).hide();
} else {
console.log('form validation feedback for '+fname+' is not visable, display main.');
}
}
});
});
});
......@@ -3,7 +3,7 @@
{{end}}
{{ define "partials/page-wrapper" }}
<div class="container">
<div class="container" id="page-content">
<div class="card o-hidden border-0 shadow-lg my-5">
<div class="card-body p-0">
......@@ -102,7 +102,7 @@
</form>
<hr>
<div class="text-center">
<a class="small" href="/user/forgot-password">Forgot Password?</a>
<a class="small" href="/user/reset-password">Forgot Password?</a>
</div>
<div class="text-center">
<a class="small" href="/user/login">Already have an account? Login!</a>
......@@ -212,4 +212,4 @@
}).change();
});
</script>
{{end}}
\ No newline at end of file
{{end}}
......@@ -3,7 +3,7 @@
{{end}}
{{ define "partials/page-wrapper" }}
<div class="container">
<div class="container" id="page-content">
<!-- Outer Row -->
<div class="row justify-content-center">
......@@ -42,7 +42,7 @@
</form>
<hr>
<div class="text-center">
<a class="small" href="/user/forgot-password">Forgot Password?</a>
<a class="small" href="/user/reset-password">Forgot Password?</a>
</div>
<div class="text-center">
<a class="small" href="/signup">Create an Account!</a>
......@@ -65,4 +65,4 @@
$(document).find('body').addClass('bg-gradient-primary');
});
</script>
{{end}}
\ No newline at end of file
{{end}}
{{define "title"}}Reset Password{{end}}
{{define "style"}}
{{end}}
{{ define "partials/page-wrapper" }}
<div class="container" id="page-content">
<!-- Outer Row -->
<div class="row justify-content-center">
<div class="col-xl-10 col-lg-12 col-md-9">
<div class="card o-hidden border-0 shadow-lg my-5">
<div class="card-body p-0">
<!-- Nested Row within Card Body -->
<div class="row">
<div class="col-lg-6 d-none d-lg-block bg-login-image"></div>
<div class="col-lg-6">
<div class="p-5">
<div class="text-center">
<h1 class="h4 text-gray-900 mb-2">Reset Your Password</h1>
<p class="mb-4">.....</p>
</div>
{{ template "validation-error" . }}
<form class="user" method="post" novalidate>
<input type="hidden" name="ResetHash" value="{{ $.form.ResetHash }}" />
<div class="form-group row">
<div class="col-sm-6 mb-3 mb-sm-0">
<input type="password" class="form-control form-control-user {{ ValidationFieldClass $.validationErrors "Password" }}" name="Password" value="{{ $.form.Password }}" placeholder="Password" required>
{{template "invalid-feedback" dict "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors "fieldName" "Password" }}
</div>
<div class="col-sm-6">
<input type="password" class="form-control form-control-user {{ ValidationFieldClass $.validationErrors "PasswordConfirm" }}" name="PasswordConfirm" value="{{ $.form.PasswordConfirm }}" placeholder="Repeat Password" required>
{{template "invalid-feedback" dict "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors "fieldName" "PasswordConfirm" }}
</div>
</div>
<button class="btn btn-primary btn-user btn-block">
Reset Password
</button>
<hr>
</form>
<hr>
<div class="text-center">
<a class="small" href="/user/login">Already have an account? Login!</a>
</div>
<div class="text-center">
<a class="small" href="/signup">Create an Account!</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{{end}}
{{define "js"}}
<script>
$(document).ready(function() {
$(document).find('body').addClass('bg-gradient-primary');
});
</script>
{{end}}
\ No newline at end of file
{{define "title"}}User Forgot Password{{end}}
{{define "style"}}
{{end}}
{{ define "partials/page-wrapper" }}
<div class="container" id="page-content">
<!-- Outer Row -->
<div class="row justify-content-center">
<div class="col-xl-10 col-lg-12 col-md-9">
<div class="card o-hidden border-0 shadow-lg my-5">
<div class="card-body p-0">
<!-- Nested Row within Card Body -->
<div class="row">
<div class="col-lg-6 d-none d-lg-block bg-login-image"></div>
<div class="col-lg-6">
<div class="p-5">
<div class="text-center">
<h1 class="h4 text-gray-900 mb-2">Forgot Your Password?</h1>
<p class="mb-4">We get it, stuff happens. Just enter your email address below and we'll send you a link to reset your password!</p>
</div>
<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">
Reset Password
</button>
<hr>
</form>
<hr>
<div class="text-center">
<a class="small" href="/user/login">Already have an account? Login!</a>
</div>
<div class="text-center">
<a class="small" href="/signup">Create an Account!</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{{end}}
{{define "js"}}
<script>
$(document).ready(function() {
$(document).find('body').addClass('bg-gradient-primary');
});
</script>
{{end}}
\ No newline at end of file
......@@ -99,7 +99,7 @@
</html>
{{end}}
{{ define "invalid-feedback" }}
<div class="invalid-feedback">
<div class="invalid-feedback" data-field="{{ .fieldName }}">
{{ if ValidationErrorHasField .validationErrors .fieldName }}
{{ range $verr := (ValidationFieldErrors .validationErrors .fieldName) }}{{ $verr.Display }}<br/>{{ end }}
{{ else }}
......@@ -127,4 +127,21 @@
</div>
{{ end }}
{{ end }}
{{ end }}
{{ define "validation-error" }}
{{ if .validationErrors }}
{{ $errMsg := (ErrorMessage $._Ctx .validationErrors) }}
{{ if $errMsg }}
<div class="alert alert-danger validation-error" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"> <span aria-hidden="true">×</span> </button> {{ if $errMsg }}<h3>{{ $errMsg }}</h3> {{end}}
{{ if .validationErrors.Fields }}
<ul>
{{ range $i := .validationErrors.Fields }}
<li data-form-field="{{ $i.FormField }}">{{ if $i.Display }}{{ $i.Display }}{{ else }}{{ $i.Error }}{{ end }}</li>
{{end}}
</ul>
{{ end }}
</div>
{{ end }}
{{ end }}
{{ end }}
\ No newline at end of file
......@@ -23,13 +23,13 @@
{{ template "top-error" . }}
<!-- ============================================================== -->
<!-- Page Content -->
<!-- ============================================================== -->
<div class="container-fluid">
<div class="container-fluid" id="page-content">
{{ template "validation-error" . }}
{{ template "content" . }}
</div>
<!-- End Page Content -->
......
......@@ -41,6 +41,7 @@ require (
github.com/sethgrid/pester v0.0.0-20190127155807-68a33a018ad0
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24
github.com/stretchr/testify v1.3.0
github.com/sudo-suhas/symcrypto v1.0.0
github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14
github.com/swaggo/swag v1.6.2
github.com/tinylib/msgp v1.1.0 // indirect
......@@ -55,6 +56,7 @@ require (
google.golang.org/appengine v1.6.1 // indirect
gopkg.in/DataDog/dd-trace-go.v1 v1.16.1
gopkg.in/go-playground/validator.v9 v9.29.1
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce
gotest.tools v2.2.0+incompatible // indirect
)
......@@ -136,6 +136,8 @@ github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoH
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/sudo-suhas/symcrypto v1.0.0 h1:VG6FdACf5XeXFQUzeA++aB6snNThz0OFlmUHiCddi2s=
github.com/sudo-suhas/symcrypto v1.0.0/go.mod h1:g/faGDjhlF/DXdqp3+SQ0LmhPcv4iYaIRjcm/Q60+68=
github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14 h1:PyYN9JH5jY9j6av01SpfRMb+1DWg/i3MbGOKPxJ2wjM=
github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14/go.mod h1:gxQT6pBGRuIGunNf/+tSOB5OHvguWi8Tbt82WOkf35E=
github.com/swaggo/swag v1.6.2 h1:WQMAtT/FmMBb7g0rAuHDhG3vvdtHKJ3WZ+Ssb0p4Y6E=
......@@ -199,6 +201,8 @@ gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXa
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
gopkg.in/go-playground/validator.v9 v9.29.1 h1:SvGtYmN60a5CVKTOzMSyfzWDeZRxRuGvRQyEAKbw1xc=
gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce h1:xcEWjVhvbDy+nHP67nPDDpbYrY+ILlfndk4bRioVHaU=
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
......