1
0
mirror of https://github.com/raseels-repos/golang-saas-starter-kit.git synced 2025-08-08 22:36:41 +02:00

issue#16 web-app account signup

Account signup works with validation and translations.
This commit is contained in:
Lee Brown
2019-07-31 13:47:30 -08:00
parent a225f9f24e
commit 227af02f31
50 changed files with 1976 additions and 427 deletions

View File

@ -15,7 +15,7 @@ import (
) )
// API returns a handler for a set of routes. // API returns a handler for a set of routes.
func API(shutdown chan os.Signal, log *log.Logger, masterDB *sqlx.DB, redis *redis.Client, authenticator *auth.Authenticator, globalMids ...web.Middleware) http.Handler { func API(shutdown chan os.Signal, log *log.Logger, env web.Env, masterDB *sqlx.DB, redis *redis.Client, authenticator *auth.Authenticator, globalMids ...web.Middleware) http.Handler {
// Define base middlewares applied to all requests. // Define base middlewares applied to all requests.
middlewares := []web.Middleware{ middlewares := []web.Middleware{
@ -28,7 +28,7 @@ func API(shutdown chan os.Signal, log *log.Logger, masterDB *sqlx.DB, redis *red
} }
// Construct the web.App which holds all routes as well as common Middleware. // Construct the web.App which holds all routes as well as common Middleware.
app := web.NewApp(shutdown, log, middlewares...) app := web.NewApp(shutdown, log, env, middlewares...)
// Register health check endpoint. This route is not authenticated. // Register health check endpoint. This route is not authenticated.
check := Check{ check := Check{

View File

@ -6,6 +6,7 @@ import (
"encoding/json" "encoding/json"
"expvar" "expvar"
"fmt" "fmt"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext"
"log" "log"
"net" "net"
"net/http" "net/http"
@ -359,8 +360,9 @@ func main() {
// ========================================================================= // =========================================================================
// Load middlewares that need to be configured specific for the service. // Load middlewares that need to be configured specific for the service.
var serviceMiddlewares = []web.Middleware{
var serviceMiddlewares []web.Middleware mid.Translator(webcontext.UniversalTranslator()),
}
// Init redirect middleware to ensure all requests go to the primary domain contained in the base URL. // Init redirect middleware to ensure all requests go to the primary domain contained in the base URL.
if primaryServiceHost != "127.0.0.1" && primaryServiceHost != "localhost" { if primaryServiceHost != "127.0.0.1" && primaryServiceHost != "localhost" {

View File

@ -22,6 +22,48 @@ an image and displays resvised versions of it on the index page. See section bel
If you would like to help, please email twins@geeksinthewoods.com. If you would like to help, please email twins@geeksinthewoods.com.
## Local Installation
### Build
```bash
go build .
```
### Docker
To build using the docker file, need to be in the project root directory. `Dockerfile` references go.mod in root directory.
```bash
docker build -f cmd/web-app/Dockerfile -t saas-web-app .
```
## Getting Started
### Errors
- **validation error** - Test by appending `test-validation-error=1` to the request URL.
http://127.0.0.1:3000/signup?test-validation-error=1
- **web error** - Test by appending `test-web-error=1` to the request URL.
http://127.0.0.1:3000/signup?test-web-error=1
### Localization
Test a specific language by appending the locale to the request URL.
127.0.0.1:3000/signup?local=fr
[github.com/go-playground/validator](https://github.com/go-playground/validator) supports the following languages.
- en - English
- fr - French
- id - Indonesian
- ja - Japanese
- nl - Dutch
- zh - Chinese
### Future Functionality ### Future Functionality
This example Web App is going to allow users to manage checklists. Users with role of admin will be allowed to This example Web App is going to allow users to manage checklists. Users with role of admin will be allowed to
@ -66,20 +108,3 @@ This web-app service eventually will include the following:
## Local Installation
### Build
```bash
go build .
```
### Docker
To build using the docker file, need to be in the project root directory. `Dockerfile` references go.mod in root directory.
```bash
docker build -f cmd/web-app/Dockerfile -t saas-web-app .
```

View File

@ -38,7 +38,7 @@ func (c *Check) Health(ctx context.Context, w http.ResponseWriter, r *http.Reque
"Status": "ok", "Status": "ok",
} }
return c.Renderer.Render(ctx, w, r, baseLayoutTmpl, "health.tmpl", web.MIMETextHTMLCharsetUTF8, http.StatusOK, data) return web.RespondJson(ctx, w, data, http.StatusOK)
} }
// Ping validates the service is ready to accept requests. // Ping validates the service is ready to accept requests.

View File

@ -0,0 +1,21 @@
package handlers
import (
"context"
"net/http"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web"
"github.com/jmoiron/sqlx"
)
// User represents the User API method handler set.
type Projects struct {
MasterDB *sqlx.DB
Renderer web.Renderer
// ADD OTHER STATE LIKE THE LOGGER AND CONFIG HERE.
}
// List returns all the existing users in the system.
func (p *Projects) Index(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
return p.Renderer.Render(ctx, w, r, tmplLayoutBase, "projects-index.tmpl", web.MIMETextHTMLCharsetUTF8, http.StatusOK, nil)
}

View File

@ -2,6 +2,7 @@ package handlers
import ( import (
"context" "context"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/auth"
"net/http" "net/http"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web" "geeks-accelerator/oss/saas-starter-kit/internal/platform/web"
@ -17,9 +18,16 @@ type Root struct {
// List returns all the existing users in the system. // List returns all the existing users in the system.
func (u *Root) Index(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error { func (u *Root) Index(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
// Force users to login to access the index page.
if claims, err := auth.ClaimsFromContext(ctx); err != nil || !claims.HasAuth() {
http.Redirect(w, r, "/user/login", http.StatusFound)
return nil
}
data := map[string]interface{}{ data := map[string]interface{}{
"imgSizes": []int{100, 200, 300, 400, 500}, "imgSizes": []int{100, 200, 300, 400, 500},
} }
return u.Renderer.Render(ctx, w, r, baseLayoutTmpl, "root-index.tmpl", web.MIMETextHTMLCharsetUTF8, http.StatusOK, data) return u.Renderer.Render(ctx, w, r, tmplLayoutBase, "root-index.tmpl", web.MIMETextHTMLCharsetUTF8, http.StatusOK, data)
} }

View File

@ -1,20 +1,28 @@
package handlers package handlers
import ( import (
"context"
"fmt"
"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/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/weberror"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
"gopkg.in/DataDog/dd-trace-go.v1/contrib/go-redis/redis" "gopkg.in/DataDog/dd-trace-go.v1/contrib/go-redis/redis"
) )
const baseLayoutTmpl = "base.tmpl" const (
tmplLayoutBase = "base.tmpl"
tmplContentErrorGeneric = "error-generic.gohtml"
)
// API returns a handler for a set of routes. // API returns a handler for a set of routes.
func APP(shutdown chan os.Signal, log *log.Logger, staticDir, templateDir string, masterDB *sqlx.DB, redis *redis.Client, 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, renderer web.Renderer, globalMids ...web.Middleware) http.Handler {
// Define base middlewares applied to all requests. // Define base middlewares applied to all requests.
middlewares := []web.Middleware{ middlewares := []web.Middleware{
@ -27,25 +35,37 @@ func APP(shutdown chan os.Signal, log *log.Logger, staticDir, templateDir string
} }
// Construct the web.App which holds all routes as well as common Middleware. // Construct the web.App which holds all routes as well as common Middleware.
app := web.NewApp(shutdown, log, middlewares...) app := web.NewApp(shutdown, log, env, middlewares...)
// Register health check endpoint. This route is not authenticated. // Register project management pages.
check := Check{ p := Projects{
MasterDB: masterDB, MasterDB: masterDB,
Redis: redis,
Renderer: renderer, Renderer: renderer,
} }
app.Handle("GET", "/v1/health", check.Health) app.Handle("GET", "/projects", p.Index, mid.HasAuth())
// Register user management and authentication endpoints. // Register user management and authentication endpoints.
u := User{ u := User{
MasterDB: masterDB, MasterDB: masterDB,
Renderer: renderer, Renderer: renderer,
Authenticator: authenticator,
} }
// This route is not authenticated // This route is not authenticated
app.Handle("POST", "/users/login", u.Login) app.Handle("POST", "/user/login", u.Login)
app.Handle("GET", "/users/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)
// Register user management and authentication endpoints.
s := Signup{
MasterDB: masterDB,
Renderer: renderer,
Authenticator: authenticator,
}
// This route is not authenticated
app.Handle("POST", "/signup", s.Step1)
app.Handle("GET", "/signup", s.Step1)
// Register root // Register root
r := Root{ r := Root{
@ -56,8 +76,32 @@ func APP(shutdown chan os.Signal, log *log.Logger, staticDir, templateDir string
app.Handle("GET", "/index.html", r.Index) app.Handle("GET", "/index.html", r.Index)
app.Handle("GET", "/", r.Index) app.Handle("GET", "/", r.Index)
// Register health check endpoint. This route is not authenticated.
check := Check{
MasterDB: masterDB,
Redis: redis,
Renderer: renderer,
}
app.Handle("GET", "/v1/health", check.Health)
static := func(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
err := web.StaticHandler(ctx, w, r, params, staticDir, "")
if err != nil {
if os.IsNotExist(err) {
rmsg := fmt.Sprintf("%s %s not found", r.Method, r.RequestURI)
err = weberror.NewErrorMessage(ctx, err, http.StatusNotFound, rmsg)
} else {
err = weberror.NewError(ctx, err, http.StatusInternalServerError)
}
return web.RenderError(ctx, w, r, err, renderer, tmplLayoutBase, tmplContentErrorGeneric, web.MIMETextHTMLCharsetUTF8)
}
return nil
}
// Static file server // Static file server
app.Handle("GET", "/*", web.Static(staticDir, "")) app.Handle("GET", "/*", static)
return app return app
} }

View File

@ -0,0 +1,88 @@
package handlers
import (
"context"
"net/http"
"time"
"geeks-accelerator/oss/saas-starter-kit/internal/account"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/auth"
"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"
"geeks-accelerator/oss/saas-starter-kit/internal/signup"
"geeks-accelerator/oss/saas-starter-kit/internal/user"
"github.com/gorilla/schema"
"github.com/jmoiron/sqlx"
"github.com/pkg/errors"
)
// Signup represents the Signup API method handler set.
type Signup struct {
MasterDB *sqlx.DB
Renderer web.Renderer
Authenticator *auth.Authenticator
}
// Step1 handles collecting the first detailed needed to create a new account.
func (h *Signup) Step1(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
//
req := new(signup.SignupRequest)
data := make(map[string]interface{})
f := func() error {
claims, _ := auth.ClaimsFromContext(ctx)
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
}
// Execute the account / user signup.
res, err := signup.Signup(ctx, claims, h.MasterDB, *req, time.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)
} else {
return err
}
}
} else {
// Authenticated the new user.
userAuth, err := user.Authenticate(ctx, h.MasterDB, h.Authenticator, res.User.Email, req.User.Password, time.Hour, time.Now())
if err != nil {
return err
}
_ = userAuth.Expiry
_ = userAuth.AccessToken
}
}
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(signup.SignupRequest{})); ok {
data["validationDefaults"] = verr.(*weberror.Error)
}
return h.Renderer.Render(ctx, w, r, tmplLayoutBase, "signup-step1.tmpl", web.MIMETextHTMLCharsetUTF8, http.StatusOK, data)
}

View File

@ -2,6 +2,7 @@ package handlers
import ( import (
"context" "context"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/auth"
"net/http" "net/http"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web" "geeks-accelerator/oss/saas-starter-kit/internal/platform/web"
@ -10,13 +11,25 @@ import (
// User represents the User API method handler set. // User represents the User API method handler set.
type User struct { type User struct {
MasterDB *sqlx.DB MasterDB *sqlx.DB
Renderer web.Renderer Renderer web.Renderer
// ADD OTHER STATE LIKE THE LOGGER AND CONFIG HERE. Authenticator *auth.Authenticator
} }
// List returns all the existing users in the system. // List returns all the existing users in the system.
func (u *User) Login(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error { func (u *User) Login(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
return u.Renderer.Render(ctx, w, r, baseLayoutTmpl, "user-login.tmpl", web.MIMETextHTMLCharsetUTF8, http.StatusOK, nil) return u.Renderer.Render(ctx, w, r, tmplLayoutBase, "user-login.tmpl", web.MIMETextHTMLCharsetUTF8, http.StatusOK, nil)
}
// List returns all the existing users in the system.
func (u *User) Logout(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
return u.Renderer.Render(ctx, w, r, tmplLayoutBase, "user-logout.tmpl", web.MIMETextHTMLCharsetUTF8, http.StatusOK, nil)
}
// List returns all the existing users in the system.
func (u *User) ForgotPassword(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
return u.Renderer.Render(ctx, w, r, tmplLayoutBase, "user-forgot-password.tmpl", web.MIMETextHTMLCharsetUTF8, http.StatusOK, nil)
} }

View File

@ -6,6 +6,9 @@ import (
"encoding/json" "encoding/json"
"expvar" "expvar"
"fmt" "fmt"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/auth"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/weberror"
"html/template" "html/template"
"log" "log"
"net" "net"
@ -341,10 +344,25 @@ func main() {
} }
defer masterDb.Close() defer masterDb.Close()
// =========================================================================
// Init new Authenticator
var authenticator *auth.Authenticator
if cfg.Auth.UseAwsSecretManager {
secretName := filepath.Join(cfg.Aws.SecretsManagerConfigPrefix, "authenticator")
authenticator, err = auth.NewAuthenticatorAws(awsSession, secretName, time.Now().UTC(), cfg.Auth.KeyExpiration)
} else {
authenticator, err = auth.NewAuthenticatorFile("", time.Now().UTC(), cfg.Auth.KeyExpiration)
}
if err != nil {
log.Fatalf("main : Constructing authenticator : %+v", err)
}
// ========================================================================= // =========================================================================
// Load middlewares that need to be configured specific for the service. // Load middlewares that need to be configured specific for the service.
var serviceMiddlewares []web.Middleware var serviceMiddlewares = []web.Middleware{
mid.Translator(webcontext.UniversalTranslator()),
}
// Init redirect middleware to ensure all requests go to the primary domain contained in the base URL. // Init redirect middleware to ensure all requests go to the primary domain contained in the base URL.
if primaryServiceHost != "127.0.0.1" && primaryServiceHost != "localhost" { if primaryServiceHost != "127.0.0.1" && primaryServiceHost != "localhost" {
@ -402,16 +420,6 @@ func main() {
var staticUrlFormatter func(string) string var staticUrlFormatter func(string) string
if cfg.Service.StaticFiles.S3Enabled || cfg.Service.StaticFiles.CloudFrontEnabled { if cfg.Service.StaticFiles.S3Enabled || cfg.Service.StaticFiles.CloudFrontEnabled {
staticUrlFormatter = staticS3UrlFormatter staticUrlFormatter = staticS3UrlFormatter
} else {
baseUrl, err := url.Parse(cfg.Service.BaseUrl)
if err != nil {
log.Fatalf("main : url Parse(%s) : %+v", cfg.Service.BaseUrl, err)
}
staticUrlFormatter = func(p string) string {
baseUrl.Path = p
return baseUrl.String()
}
} }
// ========================================================================= // =========================================================================
@ -504,6 +512,98 @@ func main() {
} }
return u return u
}, },
"ValidationErrorHasField": func(err interface{}, fieldName string) bool {
if err == nil {
return false
}
verr, ok := err.(*weberror.Error)
if !ok {
return false
}
for _, e := range verr.Fields {
if e.Field == fieldName || e.FormField == fieldName {
return true
}
}
return false
},
"ValidationFieldErrors": func(err interface{}, fieldName string) []weberror.FieldError {
if err == nil {
return []weberror.FieldError{}
}
verr, ok := err.(*weberror.Error)
if !ok {
return []weberror.FieldError{}
}
var l []weberror.FieldError
for _, e := range verr.Fields {
if e.Field == fieldName || e.FormField == fieldName {
l = append(l, e)
}
}
return l
},
"ValidationFieldClass": func(err interface{}, fieldName string) string {
if err == nil {
return ""
}
verr, ok := err.(*weberror.Error)
if !ok || len(verr.Fields) == 0 {
return ""
}
for _, e := range verr.Fields {
if e.Field == fieldName || e.FormField == fieldName {
return "is-invalid"
}
}
return "is-valid"
},
"ErrorMessage": func(ctx context.Context, err error) string {
werr, ok := err.(*weberror.Error)
if ok {
if werr.Message != "" {
return werr.Message
}
return werr.Error()
}
return fmt.Sprintf("%s", err)
},
"ErrorDetails": func(ctx context.Context, err error) string {
var displayFullError bool
switch webcontext.ContextEnv(ctx) {
case webcontext.Env_Dev, webcontext.Env_Stage:
displayFullError = true
}
if !displayFullError {
return ""
}
werr, ok := err.(*weberror.Error)
if ok {
if werr.Cause != nil {
return fmt.Sprintf("%s\n%+v", werr.Error(), werr.Cause)
}
return fmt.Sprintf("%+v", werr.Error())
}
return fmt.Sprintf("%+v", err)
},
}
imgUrlFormatter := staticUrlFormatter
if imgUrlFormatter == nil {
baseUrl, err := url.Parse(cfg.Service.BaseUrl)
if err != nil {
log.Fatalf("main : url Parse(%s) : %+v", cfg.Service.BaseUrl, err)
}
imgUrlFormatter = func(p string) string {
baseUrl.Path = p
return baseUrl.String()
}
} }
// Image Formatter - additional functions exposed to templates for resizing images // Image Formatter - additional functions exposed to templates for resizing images
@ -511,7 +611,7 @@ func main() {
imgResizeS3KeyPrefix := filepath.Join(cfg.Service.StaticFiles.S3Prefix, "images/responsive") imgResizeS3KeyPrefix := filepath.Join(cfg.Service.StaticFiles.S3Prefix, "images/responsive")
imgSrcAttr := func(ctx context.Context, p string, sizes []int, includeOrig bool) template.HTMLAttr { imgSrcAttr := func(ctx context.Context, p string, sizes []int, includeOrig bool) template.HTMLAttr {
u := staticUrlFormatter(p) u := imgUrlFormatter(p)
var srcAttr string var srcAttr string
if cfg.Service.StaticFiles.ImgResizeEnabled { if cfg.Service.StaticFiles.ImgResizeEnabled {
srcAttr, _ = img_resize.S3ImgSrc(ctx, redisClient, staticS3UrlFormatter, awsSession, cfg.Aws.S3BucketPublic, imgResizeS3KeyPrefix, u, sizes, includeOrig) srcAttr, _ = img_resize.S3ImgSrc(ctx, redisClient, staticS3UrlFormatter, awsSession, cfg.Aws.S3BucketPublic, imgResizeS3KeyPrefix, u, sizes, includeOrig)
@ -543,7 +643,7 @@ func main() {
return imgSrcAttr(ctx, p, sizes, true) return imgSrcAttr(ctx, p, sizes, true)
} }
tmplFuncs["S3ImgUrl"] = func(ctx context.Context, p string, size int) string { tmplFuncs["S3ImgUrl"] = func(ctx context.Context, p string, size int) string {
imgUrl := staticUrlFormatter(p) imgUrl := imgUrlFormatter(p)
if cfg.Service.StaticFiles.ImgResizeEnabled { if cfg.Service.StaticFiles.ImgResizeEnabled {
imgUrl, _ = img_resize.S3ImgUrl(ctx, redisClient, staticS3UrlFormatter, awsSession, cfg.Aws.S3BucketPublic, imgResizeS3KeyPrefix, imgUrl, size) imgUrl, _ = img_resize.S3ImgUrl(ctx, redisClient, staticS3UrlFormatter, awsSession, cfg.Aws.S3BucketPublic, imgResizeS3KeyPrefix, imgUrl, size)
} }
@ -635,7 +735,7 @@ func main() {
if cfg.HTTP.Host != "" { if cfg.HTTP.Host != "" {
api := http.Server{ api := http.Server{
Addr: cfg.HTTP.Host, Addr: cfg.HTTP.Host,
Handler: handlers.APP(shutdown, log, cfg.Service.StaticFiles.Dir, cfg.Service.TemplateDir, masterDb, redisClient, renderer, serviceMiddlewares...), Handler: handlers.APP(shutdown, log, cfg.Env, cfg.Service.StaticFiles.Dir, cfg.Service.TemplateDir, masterDb, redisClient, authenticator, renderer, serviceMiddlewares...),
ReadTimeout: cfg.HTTP.ReadTimeout, ReadTimeout: cfg.HTTP.ReadTimeout,
WriteTimeout: cfg.HTTP.WriteTimeout, WriteTimeout: cfg.HTTP.WriteTimeout,
MaxHeaderBytes: 1 << 20, MaxHeaderBytes: 1 << 20,
@ -652,7 +752,7 @@ func main() {
if cfg.HTTPS.Host != "" { if cfg.HTTPS.Host != "" {
api := http.Server{ api := http.Server{
Addr: cfg.HTTPS.Host, Addr: cfg.HTTPS.Host,
Handler: handlers.APP(shutdown, log, cfg.Service.StaticFiles.Dir, cfg.Service.TemplateDir, masterDb, redisClient, renderer, serviceMiddlewares...), Handler: handlers.APP(shutdown, log, cfg.Env, cfg.Service.StaticFiles.Dir, cfg.Service.TemplateDir, masterDb, redisClient, authenticator, renderer, serviceMiddlewares...),
ReadTimeout: cfg.HTTPS.ReadTimeout, ReadTimeout: cfg.HTTPS.ReadTimeout,
WriteTimeout: cfg.HTTPS.WriteTimeout, WriteTimeout: cfg.HTTPS.WriteTimeout,
MaxHeaderBytes: 1 << 20, MaxHeaderBytes: 1 << 20,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

View File

@ -0,0 +1,18 @@
{{define "title"}}Error {{ .statusCode }}{{end}}
{{define "style"}}
{{end}}
{{ define "partials/page-wrapper" }}
<div class="container-fluid">
<!-- Error Text -->
<div class="text-center">
<div class="error mx-auto" data-text="{{ .statusCode }}">{{ .statusCode }}</div>
<p class="lead text-gray-800 mb-5">{{ .errorMessage }}</p>
{{ if .fullError }}
<p class="text-gray-500 mb-0">{{ .fullError }}</p>
{{ end }}
</div>
</div>
{{ end }}

View File

@ -0,0 +1,116 @@
{{define "title"}}Crean an Account{{end}}
{{define "style"}}
{{end}}
{{ define "partials/page-wrapper" }}
<div class="container">
<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-5 d-none d-lg-block bg-register-image"></div>
<div class="col-lg-7">
<div class="p-5">
<div class="text-center">
<h1 class="h4 text-gray-900 mb-4">Create an Account!</h1>
</div>
{{ template "top-error" . }}
<form class="user" method="post" novalidate>
<div class="form-group row">
<div class="col-sm-6 mb-3 mb-sm-0">
<input type="text" class="form-control form-control-user {{ ValidationFieldClass $.validationErrors "Account.Name" }}" name="Account.Name" value="{{ $.form.Account.Name }}" placeholder="Company Name" required>
{{template "invalid-feedback" dict "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors "fieldName" "Account.Name" }}
</div>
</div>
<div class="form-group row">
<div class="col-sm-6 mb-3 mb-sm-0">
<input type="text" class="form-control form-control-user {{ ValidationFieldClass $.validationErrors "Account.Address1" }}" name="Account.Address1" value="{{ $.form.Account.Address1 }}" placeholder="Address Line 1" required>
{{template "invalid-feedback" dict "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors "fieldName" "Account.Address1" }}
</div>
<div class="col-sm-6">
<input type="text" class="form-control form-control-user {{ ValidationFieldClass $.validationErrors "Account.Address2" }}" name="Account.Address2" value="{{ $.form.Account.Address2 }}" placeholder="Address Line 2">
{{template "invalid-feedback" dict "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors "fieldName" "Account.Address2" }}
</div>
</div>
<div class="form-group row">
<div class="col-sm-6 mb-3 mb-sm-0">
<input type="text" class="form-control form-control-user {{ ValidationFieldClass $.validationErrors "Account.Zipcode" }}" name="Account.Zipcode" value="{{ $.form.Account.Zipcode }}" placeholder="Zipcode" required>
{{template "invalid-feedback" dict "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors "fieldName" "Account.Zipcode" }}
</div>
</div>
<div class="form-group row">
<div class="col-sm-6 mb-3 mb-sm-0">
<input type="text" class="form-control form-control-user {{ ValidationFieldClass $.validationErrors "Account.Country" }}" name="Account.Country" value="{{ $.form.Account.Country }}" placeholder="Country" required>
{{template "invalid-feedback" dict "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors "fieldName" "Account.Country" }}
</div>
<div class="col-sm-6 mb-3 mb-sm-0">
<input type="text" class="form-control form-control-user {{ ValidationFieldClass $.validationErrors "Account.Region" }}" name="Account.Region" value="{{ $.form.Account.Region }}" placeholder="Region" required>
{{template "invalid-feedback" dict "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors "fieldName" "Account.Region" }}
</div>
</div>
<div class="form-group row">
<div class="col-sm-6 mb-3 mb-sm-0">
<input type="text" class="form-control form-control-user {{ ValidationFieldClass $.validationErrors "Account.City" }}" name="Account.City" value="{{ $.form.Account.City }}" placeholder="City" required>
{{template "invalid-feedback" dict "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors "fieldName" "Account.City" }}
</div>
</div>
<hr>
<div class="form-group row">
<div class="col-sm-6 mb-3 mb-sm-0">
<input type="text" class="form-control form-control-user {{ ValidationFieldClass $.validationErrors "User.FirstName" }}" name="User.FirstName" value="{{ $.form.User.FirstName }}" placeholder="First Name" required>
{{template "invalid-feedback" dict "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors "fieldName" "User.FirstName" }}
</div>
<div class="col-sm-6">
<input type="text" class="form-control form-control-user {{ ValidationFieldClass $.validationErrors "User.LastName" }}" name="User.LastName" value="{{ $.form.User.LastName }}" placeholder="Last Name" required>
{{template "invalid-feedback" dict "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors "fieldName" "User.LastName" }}
</div>
</div>
<div class="form-group">
<input type="email" class="form-control form-control-user {{ ValidationFieldClass $.validationErrors "User.Email" }}" name="User.Email" value="{{ $.form.User.Email }}" placeholder="Email Address" required>
{{template "invalid-feedback" dict "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors "fieldName" "User.Email" }}
</div>
<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 "User.Password" }}" name="User.Password" value="{{ $.form.User.Password }}" placeholder="Password" required>
{{template "invalid-feedback" dict "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors "fieldName" "User.Password" }}
</div>
<div class="col-sm-6">
<input type="password" class="form-control form-control-user {{ ValidationFieldClass $.validationErrors "User.PasswordConfirm" }}" name="User.PasswordConfirm" value="{{ $.form.User.PasswordConfirm }}" placeholder="Repeat Password" required>
{{template "invalid-feedback" dict "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors "fieldName" "User.PasswordConfirm" }}
</div>
</div>
<button class="btn btn-primary btn-user btn-block">
Register Account
</button>
</form>
<hr>
<div class="text-center">
<a class="small" href="/user/forgot-password">Forgot Password?</a>
</div>
<div class="text-center">
<a class="small" href="/user/login">Already have an account? Login!</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{{end}}
{{define "js"}}
<script>
$(document).ready(function() {
$(document).find('body').addClass('bg-gradient-primary');
// console.log(($('input[name=\'Account\.Name\']').parent().find('.invalid-feedback').show()));
});
</script>
{{end}}

View File

@ -2,9 +2,65 @@
{{define "style"}} {{define "style"}}
{{end}} {{end}}
{{define "content"}} {{ define "partials/page-wrapper" }}
Login to this amazing web app <div class="container">
<!-- 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-4">Welcome Back!</h1>
</div>
<form class="user">
<div class="form-group">
<input type="email" class="form-control form-control-user" id="loginEmail" aria-describedby="emailHelp" placeholder="Enter Email Address...">
</div>
<div class="form-group">
<input type="password" class="form-control form-control-user" id="loginPassword" placeholder="Password">
</div>
<div class="form-group">
<div class="custom-control custom-checkbox small">
<input type="checkbox" class="custom-control-input" id="customCheck">
<label class="custom-control-label" for="customCheck">Remember Me</label>
</div>
</div>
<a href="index.html" class="btn btn-primary btn-user btn-block">
Login
</a>
<hr>
</form>
<hr>
<div class="text-center">
<a class="small" href="/user/forgot-password">Forgot Password?</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}} {{end}}
{{define "js"}} {{define "js"}}
<script>
$(document).ready(function() {
$(document).find('body').addClass('bg-gradient-primary');
});
</script>
{{end}} {{end}}

View File

@ -5,44 +5,126 @@
<title> <title>
{{block "title" .}}{{end}} Web App {{block "title" .}}{{end}} Web App
</title> </title>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="{{block "description" .}}{{end}} "> <meta name="description" content="{{block "description" .}}{{end}} ">
<meta name="author" content="{{block "author" .}}{{end}}"> <meta name="author" content="{{block "author" .}}{{end}}">
<meta charset="utf-8">
<link rel="icon" type="image/png" sizes="16x16" href="{{ SiteAssetUrl "/assets/images/favicon.png" }}"> <link rel="icon" type="image/png" sizes="16x16" href="{{ SiteAssetUrl "/assets/images/favicon.png" }}">
<!-- ============================================================== --> <!-- ============================================================== -->
<!-- CSS --> <!-- Custom fonts for this template -->
<!-- ============================================================== --> <!-- ============================================================== -->
<link href="{{ SiteAssetUrl "/assets/css/base.css" }}" id="theme" rel="stylesheet"> <link href="{{ SiteAssetUrl "/assets/vendor/fontawesome-free/css/all.min.css" }}" rel="stylesheet" type="text/css">
<link href="https://fonts.googleapis.com/css?family=Nunito:200,200i,300,300i,400,400i,600,600i,700,700i,800,800i,900,900i" rel="stylesheet">
<!-- ============================================================== --> <!-- ============================================================== -->
<!-- Page specific CSS --> <!-- Base styles for Start Bootstrap template SB Admin 2 -->
<!-- ============================================================== -->
{{ if eq $._Service.ENV "dev" }}
<link href="{{ SiteAssetUrl "/assets/css/sb-admin-2.css" }}" rel="stylesheet">
{{ else }}
<link href="{{ SiteAssetUrl "/assets/css/sb-admin-2.min.css" }}" rel="stylesheet">
{{ end }}
<!-- ============================================================== -->
<!-- Custom styles for this service applied to all pages -->
<!-- ============================================================== -->
<link href="{{ SiteAssetUrl "/assets/css/custom.css" }}" id="theme" rel="stylesheet">
<!-- ============================================================== -->
<!-- Page specific CSS -->
<!-- ============================================================== --> <!-- ============================================================== -->
{{block "style" .}} {{end}} {{block "style" .}} {{end}}
</head> </head>
<body> <body id="page-top">
<!-- ============================================================== -->
<!-- Page content --> {{ template "partials/page-wrapper" . }}
<!-- ============================================================== -->
{{ template "content" . }}
<!-- ============================================================== --> <!-- ============================================================== -->
<!-- footer --> <!-- Logout Modal -->
<!-- ============================================================== --> <!-- ============================================================== -->
<footer class="footer"> {{ if HasAuth $._Ctx }}
© 2019 Geeks Accelerator<br/> <div class="modal fade" id="logoutModal" tabindex="-1" role="dialog" aria-labelledby="logoutModalLabel" aria-hidden="true">
{{ template "partials/buildinfo" . }} <div class="modal-dialog" role="document">
</footer> <div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="logoutModalLabel">Ready to Leave?</h5>
<button class="close" type="button" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">Select "Logout" below if you are ready to end your current session.</div>
<div class="modal-footer">
<button class="btn btn-secondary" type="button" data-dismiss="modal">Cancel</button>
<a class="btn btn-primary" href="/logout">Logout</a>
</div>
</div>
</div>
</div>
{{ end }}
<!-- ============================================================== --> <!-- ============================================================== -->
<!-- Javascript --> <!-- Javascript Bootstrap core JavaScript -->
<!-- ============================================================== --> <!-- ============================================================== -->
<script src="{{ SiteAssetUrl "/js/base.js" }}"></script> <script src="{{ SiteAssetUrl "/assets/vendor/jquery/jquery.min.js" }}"></script>
<script src="{{ SiteAssetUrl "/assets/vendor/bootstrap/js/bootstrap.bundle.min.js" }}"></script>
<!-- ============================================================== --> <!-- ============================================================== -->
<!-- Page specific Javascript --> <!-- Core plugin JavaScript -->
<!-- ============================================================== -->
<script src="{{ SiteAssetUrl "/assets/vendor/jquery-easing/jquery.easing.min.js" }}"></script>
<!-- ============================================================== -->
<!-- Javascript for Start Bootstrap template SB Admin 2 -->
<!-- ============================================================== -->
{{ if eq $._Service.ENV "dev" }}
<script src="{{ SiteAssetUrl "/assets/js/sb-admin-2.js" }}"></script>
{{ else }}
<script src="{{ SiteAssetUrl "/assets/js/sb-admin-2.min.js" }}"></script>
{{ end }}
<!-- ============================================================== -->
<!-- Custom Javascript for this service applied to all pages -->
<!-- ============================================================== -->
<script src="{{ SiteAssetUrl "/assets/js/custom.js" }}"></script>
<!-- ============================================================== -->
<!-- Page specific Javascript -->
<!-- ============================================================== --> <!-- ============================================================== -->
{{block "js" .}} {{end}} {{block "js" .}} {{end}}
</body> </body>
</html> </html>
{{end}} {{end}}
{{ define "invalid-feedback" }}
<div class="invalid-feedback">
{{ if ValidationErrorHasField .validationErrors .fieldName }}
{{ range $verr := (ValidationFieldErrors .validationErrors .fieldName) }}{{ $verr.Display }}<br/>{{ end }}
{{ else }}
{{ range $verr := (ValidationFieldErrors .validationDefaults .fieldName) }}{{ $verr.Display }}<br/>{{ end }}
{{ end }}
</div>
{{ end }}
{{ define "top-error" }}
{{ if .error }}
{{ $errMsg := (ErrorMessage $._Ctx .error) }}
{{ $errDetails := (ErrorDetails $._Ctx .error) }}
{{ if or ($errMsg) ($errDetails) }}
<div class="alert alert-danger" 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 .error.Fields }}
<ul>
{{ range $i := .error.Fields }}
<li>{{ if $i.Display }}{{ $i.Display }}{{ else }}{{ $i.Error }}{{ end }}</li>
{{end}}
</ul>
{{ end }}
{{ if $errDetails }}
<p><small>{{ $errDetails }}</small></p>
{{ end }}
</div>
{{ end }}
{{ end }}
{{ end }}

View File

@ -0,0 +1,66 @@
{{ define "partials/page-wrapper" }}
<!-- ============================================================== -->
<!-- Page Wrapper -->
<!-- ============================================================== -->
<div id="wrapper">
{{ template "partials/sidebar" . }}
<!-- ============================================================== -->
<!-- Content Wrapper -->
<!-- ============================================================== -->
<div id="content-wrapper" class="d-flex flex-column">
<!-- ============================================================== -->
<!-- Main Content -->
<!-- ============================================================== -->
<div id="content">
<!-- ============================================================== -->
<!-- Topbar -->
<!-- ============================================================== -->
{{ template "partials/topbar" . }}
<!-- End of Topbar -->
{{ template "top-error" . }}
<!-- ============================================================== -->
<!-- Page Content -->
<!-- ============================================================== -->
<div class="container-fluid">
{{ template "content" . }}
</div>
<!-- End Page Content -->
</div>
<!-- End of Main Content -->
<!-- ============================================================== -->
<!-- Footer -->
<!-- ============================================================== -->
<footer class="sticky-footer bg-white">
<div class="container my-auto">
<div class="copyright text-center my-auto">
<span>
Copyright &copy; Geeks Accelerator 2019<br/>
{{ template "partials/buildinfo" . }}
</span>
</div>
</div>
</footer>
<!-- End of Footer -->
</div>
<!-- End of Content Wrapper -->
</div>
<!-- End of Page Wrapper -->
<!-- Scroll to Top Button-->
<a class="scroll-to-top rounded" href="#page-top">
<i class="fas fa-angle-up"></i>
</a>
{{ end }}

View File

@ -0,0 +1,68 @@
{{ define "partials/sidebar" }}
<!-- Sidebar -->
<ul class="navbar-nav bg-gradient-primary sidebar sidebar-dark accordion" id="accordionSidebar">
<!-- Sidebar - Brand -->
<a class="sidebar-brand d-flex align-items-center justify-content-center" href="/">
<div class="sidebar-brand-icon rotate-n-15">
<i class="fas fa-dragon"></i>
</div>
<div class="sidebar-brand-text mx-3">Example Project</div>
</a>
<!-- Divider -->
<hr class="sidebar-divider my-0">
<!-- Nav Item - Dashboard -->
<li class="nav-item">
<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>
<!-- Divider -->
<hr class="sidebar-divider d-none d-md-block">
<!-- Sidebar Toggler (Sidebar) -->
<div class="text-center d-none d-md-inline">
<button class="rounded-circle border-0" id="sidebarToggle"></button>
</div>
</ul>
<!-- End of Sidebar -->
{{ end }}

View File

@ -0,0 +1,184 @@
{{ define "partials/topbar" }}
<!-- Topbar -->
<nav class="navbar navbar-expand navbar-light bg-white topbar mb-4 static-top shadow">
<!-- Sidebar Toggle (Topbar) -->
<button id="sidebarToggleTop" class="btn btn-link d-md-none rounded-circle mr-3">
<i class="fa fa-bars"></i>
</button>
<!-- Topbar Search -->
<form class="d-none d-sm-inline-block form-inline mr-auto ml-md-3 my-2 my-md-0 mw-100 navbar-search">
<div class="input-group">
<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">
<button class="btn btn-primary" type="button">
<i class="fas fa-search fa-sm"></i>
</button>
</div>
</div>
</form>
<!-- Topbar Navbar -->
<ul class="navbar-nav ml-auto">
<!-- Nav Item - Search Dropdown (Visible Only XS) -->
<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">
<i class="fas fa-search fa-fw"></i>
</a>
<!-- Dropdown - Messages -->
<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">
<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">
<div class="input-group-append">
<button class="btn btn-primary" type="button">
<i class="fas fa-search fa-sm"></i>
</button>
</div>
</div>
</form>
</div>
</li>
<!-- Nav Item - Alerts -->
<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">
<i class="fas fa-bell fa-fw"></i>
<!-- Counter - Alerts -->
<span class="badge badge-danger badge-counter">3+</span>
</a>
<!-- Dropdown - Alerts -->
<div class="dropdown-list dropdown-menu dropdown-menu-right shadow animated--grow-in" aria-labelledby="alertsDropdown">
<h6 class="dropdown-header">
Alerts Center
</h6>
<a class="dropdown-item d-flex align-items-center" href="#">
<div class="mr-3">
<div class="icon-circle bg-primary">
<i class="fas fa-file-alt text-white"></i>
</div>
</div>
<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>
</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>
<div class="small text-gray-500">December 7, 2019</div>
$290.29 has been deposited into your account!
</div>
</a>
<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 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 -->
<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">
<i class="fas fa-envelope fa-fw"></i>
<!-- Counter - Messages -->
<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 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/AU4VPcFN4LE/60x60" alt="">
<div class="status-indicator"></div>
</div>
<div>
<div class="text-truncate">I have the photos that you ordered last month, how would you like them sent to you?</div>
<div class="small text-gray-500">Jae Chun · 1d</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/CS2uCrpNzJY/60x60" alt="">
<div class="status-indicator bg-warning"></div>
</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>
<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>
<!-- Nav Item - User Information -->
<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">
<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">
</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 class="dropdown-item" href="#">
<i class="fas fa-cogs fa-sm fa-fw mr-2 text-gray-400"></i>
Settings
</a>
<a class="dropdown-item" href="#">
<i class="fas fa-list fa-sm fa-fw mr-2 text-gray-400"></i>
Activity Log
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="#" data-toggle="modal" data-target="#logoutModal">
<i class="fas fa-sign-out-alt fa-sm fa-fw mr-2 text-gray-400"></i>
Logout
</a>
</div>
</li>
</ul>
</nav>
<!-- End of Topbar -->
{{ end }}

27
go.mod
View File

@ -1,31 +1,33 @@
module geeks-accelerator/oss/saas-starter-kit module geeks-accelerator/oss/saas-starter-kit
require ( require (
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751
github.com/aws/aws-sdk-go v1.20.16 github.com/aws/aws-sdk-go v1.21.8
github.com/bobesa/go-domain-util v0.0.0-20180815122459-1d708c097a6a github.com/bobesa/go-domain-util v0.0.0-20180815122459-1d708c097a6a
github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/dimfeld/httptreemux v5.0.1+incompatible github.com/dimfeld/httptreemux v5.0.1+incompatible
github.com/dustin/go-humanize v1.0.0 github.com/dustin/go-humanize v1.0.0
github.com/fatih/camelcase v1.0.0 github.com/fatih/camelcase v1.0.0
github.com/fatih/structtag v1.0.0 github.com/fatih/structtag v1.0.0
github.com/fsnotify/fsnotify v1.4.7
github.com/geeks-accelerator/sqlxmigrate v0.0.0-20190527223850-4a863a2d30db github.com/geeks-accelerator/sqlxmigrate v0.0.0-20190527223850-4a863a2d30db
github.com/go-openapi/spec v0.19.2 // indirect github.com/go-openapi/spec v0.19.2 // indirect
github.com/go-openapi/swag v0.19.4 // indirect github.com/go-openapi/swag v0.19.4 // indirect
github.com/go-playground/locales v0.12.1 github.com/go-playground/locales v0.12.1
github.com/go-playground/pkg v0.0.0-20190522230805-792a755e6910
github.com/go-playground/universal-translator v0.16.0 github.com/go-playground/universal-translator v0.16.0
github.com/go-redis/redis v6.15.2+incompatible github.com/go-redis/redis v6.15.2+incompatible
github.com/golang/protobuf v1.3.2 // indirect
github.com/google/go-cmp v0.3.0 github.com/google/go-cmp v0.3.0
github.com/google/uuid v1.1.1 // indirect github.com/google/uuid v1.1.1 // indirect
github.com/gorilla/schema v1.1.0 github.com/gorilla/schema v1.1.0
github.com/huandu/go-sqlbuilder v1.4.0 github.com/huandu/go-sqlbuilder v1.4.1
github.com/iancoleman/strcase v0.0.0-20190422225806-e506e3ef7365 github.com/iancoleman/strcase v0.0.0-20190422225806-e506e3ef7365
github.com/jmoiron/sqlx v1.2.0 github.com/jmoiron/sqlx v1.2.0
github.com/kelseyhightower/envconfig v1.4.0 github.com/kelseyhightower/envconfig v1.4.0
github.com/leodido/go-urn v1.1.0 // indirect github.com/leodido/go-urn v1.1.0 // indirect
github.com/lib/pq v1.1.1 github.com/lib/pq v1.2.0
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e // indirect github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e // indirect
github.com/mattn/go-sqlite3 v1.11.0 // indirect
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
github.com/onsi/ginkgo v1.8.0 // indirect github.com/onsi/ginkgo v1.8.0 // indirect
github.com/onsi/gomega v1.5.0 github.com/onsi/gomega v1.5.0
@ -37,18 +39,19 @@ require (
github.com/sethgrid/pester v0.0.0-20190127155807-68a33a018ad0 github.com/sethgrid/pester v0.0.0-20190127155807-68a33a018ad0
github.com/stretchr/testify v1.3.0 github.com/stretchr/testify v1.3.0
github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14 github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14
github.com/swaggo/swag v1.6.1 github.com/swaggo/swag v1.6.2
github.com/tinylib/msgp v1.1.0 // indirect github.com/tinylib/msgp v1.1.0 // indirect
github.com/urfave/cli v1.20.0 github.com/urfave/cli v1.20.0
github.com/xwb1989/sqlparser v0.0.0-20180606152119-120387863bf2 github.com/xwb1989/sqlparser v0.0.0-20180606152119-120387863bf2
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4
golang.org/x/net v0.0.0-20190628185345-da137c7871d7 // indirect golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 // indirect
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb // indirect golang.org/x/sys v0.0.0-20190730183949-1393eb018365 // indirect
golang.org/x/tools v0.0.0-20190708203411-c8855242db9c // indirect golang.org/x/text v0.3.2
golang.org/x/tools v0.0.0-20190730205120-7deaedd405c4 // indirect
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 // indirect
google.golang.org/appengine v1.6.1 // indirect google.golang.org/appengine v1.6.1 // indirect
gopkg.in/DataDog/dd-trace-go.v1 v1.15.0 gopkg.in/DataDog/dd-trace-go.v1 v1.16.1
gopkg.in/go-playground/assert.v1 v1.2.1 // indirect gopkg.in/go-playground/validator.v9 v9.29.1
gopkg.in/go-playground/validator.v9 v9.29.0
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce
gotest.tools v2.2.0+incompatible // indirect gotest.tools v2.2.0+incompatible // indirect
) )

50
go.sum
View File

@ -5,10 +5,10 @@ github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tN
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/aws/aws-sdk-go v1.20.16 h1:Dq68fBH39XnSjjb2hX/iW6mui8JtXcVAuhRYGSRiisY= github.com/aws/aws-sdk-go v1.21.8 h1:Lv6hW2twBhC6mGZAuWtqplEpIIqtVctJg02sE7Qn0Zw=
github.com/aws/aws-sdk-go v1.20.16/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.21.8/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/bobesa/go-domain-util v0.0.0-20180815122459-1d708c097a6a/go.mod h1:/mf0HzRK9xVv+1puqGSMzCo7bhEcQhiisuUXlMkq2p4= github.com/bobesa/go-domain-util v0.0.0-20180815122459-1d708c097a6a/go.mod h1:/mf0HzRK9xVv+1puqGSMzCo7bhEcQhiisuUXlMkq2p4=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -45,8 +45,12 @@ github.com/go-openapi/swag v0.19.2 h1:jvO6bCMBEilGwMfHhrd61zIID4oIFdwb76V17SM88d
github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.4 h1:i/65mCM9s1h8eCkT07F5Z/C1e/f8VTgEwer+00yevpA= github.com/go-openapi/swag v0.19.4 h1:i/65mCM9s1h8eCkT07F5Z/C1e/f8VTgEwer+00yevpA=
github.com/go-openapi/swag v0.19.4/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.4/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-playground/form v3.1.4+incompatible h1:lvKiHVxE2WvzDIoyMnWcjyiBxKt2+uFJyZcPYWsLnjI=
github.com/go-playground/form v3.1.4+incompatible/go.mod h1:lhcKXfTuhRtIZCIKUeJ0b5F207aeQCPbZU09ScKjwWg=
github.com/go-playground/locales v0.12.1 h1:2FITxuFt/xuCNP1Acdhv62OzaCiviiE4kotfhkmOqEc= github.com/go-playground/locales v0.12.1 h1:2FITxuFt/xuCNP1Acdhv62OzaCiviiE4kotfhkmOqEc=
github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
github.com/go-playground/pkg v0.0.0-20190522230805-792a755e6910 h1:h7toKaxfg9ttAloheYEndInQhXwOC/Knglt0L5MMVCM=
github.com/go-playground/pkg v0.0.0-20190522230805-792a755e6910/go.mod h1:Wg1j+HqWLhhVIfYdaoOuBzdutBEVcqwvBxgFZRWbybk=
github.com/go-playground/universal-translator v0.16.0 h1:X++omBR/4cE2MNg91AoC3rmGrCjJ8eAeUP/K/EKx4DM= github.com/go-playground/universal-translator v0.16.0 h1:X++omBR/4cE2MNg91AoC3rmGrCjJ8eAeUP/K/EKx4DM=
github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
github.com/go-redis/redis v6.15.2+incompatible h1:9SpNVG76gr6InJGxoZ6IuuxaCOQwDAhzyXg+Bs+0Sb4= github.com/go-redis/redis v6.15.2+incompatible h1:9SpNVG76gr6InJGxoZ6IuuxaCOQwDAhzyXg+Bs+0Sb4=
@ -58,6 +62,8 @@ github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@ -67,8 +73,8 @@ github.com/gorilla/schema v1.1.0 h1:CamqUDOFUBqzrvxuz2vEwo8+SUdwsluFh7IlzJh30LY=
github.com/gorilla/schema v1.1.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= github.com/gorilla/schema v1.1.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/huandu/go-sqlbuilder v1.4.0 h1:2LIlTDOz63lOETLOIiKBPEu4PUbikmS5LUc3EekwYqM= github.com/huandu/go-sqlbuilder v1.4.1 h1:DYGFGLbOUXhtQ2kwO1uyDIPJbsztmVWdPPDyxi0EJGw=
github.com/huandu/go-sqlbuilder v1.4.0/go.mod h1:mYfGcZTUS6yJsahUQ3imkYSkGGT3A+owd54+79kkW+U= github.com/huandu/go-sqlbuilder v1.4.1/go.mod h1:mYfGcZTUS6yJsahUQ3imkYSkGGT3A+owd54+79kkW+U=
github.com/iancoleman/strcase v0.0.0-20190422225806-e506e3ef7365 h1:ECW73yc9MY7935nNYXUkK7Dz17YuSUI9yqRqYS8aBww= github.com/iancoleman/strcase v0.0.0-20190422225806-e506e3ef7365 h1:ECW73yc9MY7935nNYXUkK7Dz17YuSUI9yqRqYS8aBww=
github.com/iancoleman/strcase v0.0.0-20190422225806-e506e3ef7365/go.mod h1:SK73tn/9oHe+/Y0h39VT4UCxmurVJkR5NA7kMEAOgSE= github.com/iancoleman/strcase v0.0.0-20190422225806-e506e3ef7365/go.mod h1:SK73tn/9oHe+/Y0h39VT4UCxmurVJkR5NA7kMEAOgSE=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
@ -89,8 +95,8 @@ github.com/leodido/go-urn v1.1.0 h1:Sm1gr51B1kKyfD2BlRcLSiEkffoG96g6TPv6eRoEiB8=
github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A= github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4= github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e h1:hB2xlXdHp/pmPZq0y3QnmWAArdw9PqbmotexnWx/FU8= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e h1:hB2xlXdHp/pmPZq0y3QnmWAArdw9PqbmotexnWx/FU8=
@ -98,6 +104,8 @@ github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o= github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o=
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.11.0 h1:LDdKkqtYlom37fkvqs8rMPFKAMe8+SgjbwZ6ex1/A/Q=
github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@ -126,8 +134,8 @@ github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14 h1:PyYN9JH5jY9j6av01SpfRMb+1DWg/i3MbGOKPxJ2wjM= 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/files v0.0.0-20190704085106-630677cd5c14/go.mod h1:gxQT6pBGRuIGunNf/+tSOB5OHvguWi8Tbt82WOkf35E=
github.com/swaggo/swag v1.6.1 h1:r5kS0vSmXYrSBSNdCLgGV40DpAPzSwvuLNMVIR8y0Ic= github.com/swaggo/swag v1.6.2 h1:WQMAtT/FmMBb7g0rAuHDhG3vvdtHKJ3WZ+Ssb0p4Y6E=
github.com/swaggo/swag v1.6.1/go.mod h1:YyZstMc22WYm6GEDx/CYWxq+faBbjQ5EqwQcrjREDBo= github.com/swaggo/swag v1.6.2/go.mod h1:YyZstMc22WYm6GEDx/CYWxq+faBbjQ5EqwQcrjREDBo=
github.com/tinylib/msgp v1.1.0 h1:9fQd+ICuRIu/ue4vxJZu6/LzxN0HwMds2nq/0cFvxHU= github.com/tinylib/msgp v1.1.0 h1:9fQd+ICuRIu/ue4vxJZu6/LzxN0HwMds2nq/0cFvxHU=
github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw= github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw=
@ -149,8 +157,8 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7 h1:rTIdg5QFRR7XCaK4LCjBiPbx8j4DQRpdYMnGn/bJUEU= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 h1:Ao/3l156eZf2AW5wK8a7/smtodRU+gha3+BeqJ69lRk=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -159,8 +167,8 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb h1:fgwFCsaw9buMuxNd6+DQfAuSFqbNiQZpcgJQAgJsK6k= golang.org/x/sys v0.0.0-20190730183949-1393eb018365 h1:SaXEMXhWzMJThc05vu6uh61Q245r4KaWMrsTedk0FDc=
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190730183949-1393eb018365/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
@ -169,13 +177,15 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
golang.org/x/tools v0.0.0-20190606050223-4d9ae51c2468/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190606050223-4d9ae51c2468/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190708203411-c8855242db9c h1:rRFNgkkT7zOyWlroLBmsrKYtBNhox8WtulQlOr3jIDk= golang.org/x/tools v0.0.0-20190730205120-7deaedd405c4 h1:GhbPrljMrt6gCNHHAJcWLDV3nDPFkIm0EEuqY9GtuX0=
golang.org/x/tools v0.0.0-20190708203411-c8855242db9c/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= golang.org/x/tools v0.0.0-20190730205120-7deaedd405c4/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1 h1:QzqyMA1tlu6CgqCDUtU9V+ZKhLFT2dkJuANu5QaxI3I= google.golang.org/appengine v1.6.1 h1:QzqyMA1tlu6CgqCDUtU9V+ZKhLFT2dkJuANu5QaxI3I=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
gopkg.in/DataDog/dd-trace-go.v1 v1.15.0 h1:2LhklnAJsRSelbnBrrE5QuRleRDkmOh2JWxOtIX6yec= gopkg.in/DataDog/dd-trace-go.v1 v1.16.1 h1:Dngw1zun6yTYFHNdzEWBlrJzFA2QJMjSA2sZ4nH2UWo=
gopkg.in/DataDog/dd-trace-go.v1 v1.15.0/go.mod h1:DVp8HmDh8PuTu2Z0fVVlBsyWaC++fzwVCaGWylTe3tg= gopkg.in/DataDog/dd-trace-go.v1 v1.16.1/go.mod h1:DVp8HmDh8PuTu2Z0fVVlBsyWaC++fzwVCaGWylTe3tg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@ -183,8 +193,8 @@ gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM=
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
gopkg.in/go-playground/validator.v9 v9.29.0 h1:5ofssLNYgAA/inWn6rTZ4juWpRJUwEnXc1LG2IeXwgQ= gopkg.in/go-playground/validator.v9 v9.29.1 h1:SvGtYmN60a5CVKTOzMSyfzWDeZRxRuGvRQyEAKbw1xc=
gopkg.in/go-playground/validator.v9 v9.29.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce h1:xcEWjVhvbDy+nHP67nPDDpbYrY+ILlfndk4bRioVHaU= 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/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=

View File

@ -3,7 +3,7 @@ package account
import ( import (
"context" "context"
"database/sql" "database/sql"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web" "geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext"
"time" "time"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/auth" "geeks-accelerator/oss/saas-starter-kit/internal/platform/auth"
@ -274,7 +274,7 @@ func Create(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req Accoun
return uniq return uniq
} }
v := web.NewValidator() v := webcontext.Validator()
v.RegisterValidation("unique", f) v.RegisterValidation("unique", f)
// Validate the request. // Validate the request.
@ -369,7 +369,7 @@ func Update(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req Accoun
span, ctx := tracer.StartSpanFromContext(ctx, "internal.account.Update") span, ctx := tracer.StartSpanFromContext(ctx, "internal.account.Update")
defer span.Finish() defer span.Finish()
v := web.NewValidator() v := webcontext.Validator()
// Validation name is unique in the database. // Validation name is unique in the database.
if req.Name != nil { if req.Name != nil {
@ -497,7 +497,7 @@ func Archive(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req Accou
defer span.Finish() defer span.Finish()
// Validate the request. // Validate the request.
v := web.NewValidator() v := webcontext.Validator()
err := v.Struct(req) err := v.Struct(req)
if err != nil { if err != nil {
return err return err
@ -576,7 +576,7 @@ func Delete(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, accountID
} }
// Validate the request. // Validate the request.
v := web.NewValidator() v := webcontext.Validator()
err := v.Struct(req) err := v.Struct(req)
if err != nil { if err != nil {
return err return err

View File

@ -2,6 +2,7 @@ package mid
import ( import (
"context" "context"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/weberror"
"net/http" "net/http"
"strings" "strings"
@ -11,12 +12,14 @@ import (
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
) )
// ErrForbidden is returned when an authenticated user does not have a // ErrorForbidden is returned when an authenticated user does not have a
// sufficient role for an action. // sufficient role for an action.
var ErrForbidden = web.NewRequestError( func ErrorForbidden(ctx context.Context) error {
errors.New("you are not authorized for that action"), return weberror.NewError(ctx,
http.StatusForbidden, errors.New("you are not authorized for that action"),
) http.StatusForbidden,
)
}
// Authenticate validates a JWT from the `Authorization` header. // Authenticate validates a JWT from the `Authorization` header.
func Authenticate(authenticator *auth.Authenticator) web.Middleware { func Authenticate(authenticator *auth.Authenticator) web.Middleware {
@ -33,17 +36,17 @@ func Authenticate(authenticator *auth.Authenticator) web.Middleware {
authHdr := r.Header.Get("Authorization") authHdr := r.Header.Get("Authorization")
if authHdr == "" { if authHdr == "" {
err := errors.New("missing Authorization header") err := errors.New("missing Authorization header")
return web.NewRequestError(err, http.StatusUnauthorized) return weberror.NewError(ctx, err, http.StatusUnauthorized)
} }
tknStr, err := parseAuthHeader(authHdr) tknStr, err := parseAuthHeader(authHdr)
if err != nil { if err != nil {
return web.NewRequestError(err, http.StatusUnauthorized) return weberror.NewError(ctx, err, http.StatusUnauthorized)
} }
claims, err := authenticator.ParseClaims(tknStr) claims, err := authenticator.ParseClaims(tknStr)
if err != nil { if err != nil {
return web.NewRequestError(err, http.StatusUnauthorized) return weberror.NewError(ctx, err, http.StatusUnauthorized)
} }
// Add claims to the context so they can be retrieved later. // Add claims to the context so they can be retrieved later.
@ -68,6 +71,45 @@ func Authenticate(authenticator *auth.Authenticator) web.Middleware {
return f return f
} }
// HasAuth validates the current user is an authenticated user,
func HasAuth() web.Middleware {
// This is the actual middleware function to be executed.
f := func(after web.Handler) web.Handler {
h := func(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
span, ctx := tracer.StartSpanFromContext(ctx, "internal.mid.HasAuth")
defer span.Finish()
m := func() error {
claims, err := auth.ClaimsFromContext(ctx)
if err != nil {
return err
}
if !claims.HasAuth() {
return ErrorForbidden(ctx)
}
return nil
}
if err := m(); err != nil {
if web.RequestIsJson(r) {
return web.RespondJsonError(ctx, w, err)
}
return err
}
return after(ctx, w, r, params)
}
return h
}
return f
}
// HasRole validates that an authenticated user has at least one role from a // HasRole validates that an authenticated user has at least one role from a
// specified list. This method constructs the actual function that is used. // specified list. This method constructs the actual function that is used.
func HasRole(roles ...string) web.Middleware { func HasRole(roles ...string) web.Middleware {
@ -80,14 +122,13 @@ func HasRole(roles ...string) web.Middleware {
defer span.Finish() defer span.Finish()
m := func() error { m := func() error {
claims, ok := ctx.Value(auth.Key).(auth.Claims) claims, err := auth.ClaimsFromContext(ctx)
if !ok { if err != nil {
// TODO(jlw) should this be a web.Shutdown? return err
return errors.New("claims missing from context: HasRole called without/before Authenticate")
} }
if !claims.HasRole(roles...) { if !claims.HasRole(roles...) {
return ErrForbidden return ErrorForbidden(ctx)
} }
return nil return nil

View File

@ -0,0 +1,45 @@
package mid
import (
"context"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
"net/http"
)
// CustomContext sets a default set of context values.
func CustomContext(values map[interface{}]interface{}) web.Middleware {
// This is the actual middleware function to be executed.
f := func(after web.Handler) web.Handler {
// Wrap this handler around the next one provided.
h := func(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
span, ctx := tracer.StartSpanFromContext(ctx, "internal.mid.CustomContext")
defer span.Finish()
m := func() error {
for k, v := range values {
if cv := ctx.Value(k); cv == nil {
ctx = context.WithValue(ctx, k, v)
}
}
return nil
}
if err := m(); err != nil {
if web.RequestIsJson(r) {
return web.RespondJsonError(ctx, w, err)
}
return err
}
return after(ctx, w, r, params)
}
return h
}
return f
}

View File

@ -6,6 +6,7 @@ import (
"net/http" "net/http"
"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/weberror"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
) )
@ -40,7 +41,7 @@ func Errors(log *log.Logger) web.Middleware {
// If we receive the shutdown err we need to return it // If we receive the shutdown err we need to return it
// back to the base handler to shutdown the service. // back to the base handler to shutdown the service.
if ok := web.IsShutdown(err); ok { if ok := weberror.IsShutdown(err); ok {
return err return err
} }
} }

View File

@ -2,6 +2,7 @@ package mid
import ( import (
"context" "context"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext"
"log" "log"
"net/http" "net/http"
"time" "time"
@ -22,14 +23,12 @@ func Logger(log *log.Logger) web.Middleware {
span, ctx := tracer.StartSpanFromContext(ctx, "internal.mid.Logger") span, ctx := tracer.StartSpanFromContext(ctx, "internal.mid.Logger")
defer span.Finish() defer span.Finish()
// If the context is missing this value, request the service v, err := webcontext.ContextValues(ctx)
// to be shutdown gracefully. if err != nil {
v, ok := ctx.Value(web.KeyValues).(*web.Values) return err
if !ok {
return web.NewShutdownError("web value missing from context")
} }
err := before(ctx, w, r, params) err = before(ctx, w, r, params)
log.Printf("%d : (%d) : %s %s -> %s (%s)\n", log.Printf("%d : (%d) : %s %s -> %s (%s)\n",
span.Context().TraceID(), span.Context().TraceID(),

View File

@ -3,11 +3,13 @@ package mid
import ( import (
"context" "context"
"fmt" "fmt"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext"
"net/http"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web" "geeks-accelerator/oss/saas-starter-kit/internal/platform/web"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
"net/http"
) )
// Trace adds the base tracing info for requests // Trace adds the base tracing info for requests
@ -35,17 +37,17 @@ func Trace() web.Middleware {
span, ctx := tracer.StartSpanFromContext(ctx, "http.request", opts...) span, ctx := tracer.StartSpanFromContext(ctx, "http.request", opts...)
defer span.Finish() defer span.Finish()
// If the context is missing this value, request the service // Load the context values.
// to be shutdown gracefully. v, err := webcontext.ContextValues(ctx)
v, ok := ctx.Value(web.KeyValues).(*web.Values) if err != nil {
if !ok { return err
return web.NewShutdownError("web value missing from context")
} }
v.TraceID = span.Context().TraceID() v.TraceID = span.Context().TraceID()
v.SpanID = span.Context().SpanID() v.SpanID = span.Context().SpanID()
// Execute the request handler // Execute the request handler
err := before(ctx, w, r, params) err = before(ctx, w, r, params)
// Set the span status code for the trace // Set the span status code for the trace
span.SetTag(ext.HTTPCode, v.StatusCode) span.SetTag(ext.HTTPCode, v.StatusCode)

View File

@ -0,0 +1,62 @@
package mid
import (
"context"
"net/http"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext"
httpext "github.com/go-playground/pkg/net/http"
ut "github.com/go-playground/universal-translator"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
)
func Translator(utrans *ut.UniversalTranslator) web.Middleware {
// This is the actual middleware function to be executed.
f := func(after web.Handler) web.Handler {
// Wrap this handler around the next one provided.
h := func(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
span, ctx := tracer.StartSpanFromContext(ctx, "internal.mid.Translator")
defer span.Finish()
m := func() error {
locale, _ := params["locale"]
locale = "fr"
var t ut.Translator
if len(locale) > 0 {
var found bool
if t, found = utrans.GetTranslator(locale); found {
goto END
}
}
// get and parse the "Accept-Language" http header and return an array
t, _ = utrans.FindTranslator(httpext.AcceptedLanguages(r)...)
END:
ctx = webcontext.ContextWithTranslator(ctx, t)
return nil
}
if err := m(); err != nil {
if web.RequestIsJson(r) {
return web.RespondJsonError(ctx, w, err)
}
return err
}
return after(ctx, w, r, params)
}
return h
}
return f
}

View File

@ -1,6 +1,7 @@
package auth package auth
import ( import (
"context"
"fmt" "fmt"
"time" "time"
@ -66,6 +67,14 @@ func (c Claims) Valid() error {
return nil return nil
} }
// HasAuth returns true the user is authenticated.
func (c Claims) HasAuth() bool {
if c.Subject != "" {
return true
}
return false
}
// HasRole returns true if the claims has at least one of the provided roles. // HasRole returns true if the claims has at least one of the provided roles.
func (c Claims) HasRole(roles ...string) bool { func (c Claims) HasRole(roles ...string) bool {
for _, has := range c.Roles { for _, has := range c.Roles {
@ -78,9 +87,21 @@ func (c Claims) HasRole(roles ...string) bool {
return false return false
} }
// TimeLocation returns the timezone used to format datetimes for the user.
func (c Claims) TimeLocation() *time.Location { func (c Claims) TimeLocation() *time.Location {
if c.tz == nil && c.Timezone != "" { if c.tz == nil && c.Timezone != "" {
c.tz, _ = time.LoadLocation(c.Timezone) c.tz, _ = time.LoadLocation(c.Timezone)
} }
return c.tz return c.tz
} }
// ClaimsFromContext loads the claims from context.
func ClaimsFromContext(ctx context.Context) (Claims, error) {
claims, ok := ctx.Value(Key).(Claims)
if !ok {
// TODO(jlw) should this be a web.Shutdown?
return Claims{}, errors.New("claims missing from context: HasRole called without/before Authenticate")
}
return claims, nil
}

View File

@ -1,99 +0,0 @@
package web
import (
"github.com/pkg/errors"
"gopkg.in/go-playground/validator.v9"
"net/http"
)
// FieldError is used to indicate an error with a specific request field.
type FieldError struct {
Field string `json:"field"`
Error string `json:"error"`
}
// ErrorResponse is the form used for API responses from failures in the API.
type ErrorResponse struct {
Error string `json:"error"`
Fields []FieldError `json:"fields,omitempty"`
}
// Error is used to pass an error during the request through the
// application with web specific context.
type Error struct {
Err error
Status int
Fields []FieldError
}
// NewRequestError wraps a provided error with an HTTP status code. This
// function should be used when handlers encounter expected errors.
func NewRequestError(err error, status int) error {
// if its a validation error then
if verr, ok := NewValidationError(err); ok {
return verr
}
return &Error{err, status, nil}
}
// Error implements the error interface. It uses the default message of the
// wrapped error. This is what will be shown in the services' logs.
func (err *Error) Error() string {
return err.Err.Error()
}
// shutdown is a type used to help with the graceful termination of the service.
type shutdown struct {
Message string
}
// Error is the implementation of the error interface.
func (s *shutdown) Error() string {
return s.Message
}
// NewShutdownError returns an error that causes the framework to signal
// a graceful shutdown.
func NewShutdownError(message string) error {
return &shutdown{message}
}
// NewValidationError checks the error for validation errors and formats the correct response.
func NewValidationError(err error) (error, bool) {
// Use a type assertion to get the real error value.
verrors, ok := errors.Cause(err).(validator.ValidationErrors)
if !ok {
return err, false
}
// lang controls the language of the error messages. You could look at the
// Accept-Language header if you intend to support multiple languages.
lang, _ := translator.GetTranslator("en")
var fields []FieldError
for _, verror := range verrors {
field := FieldError{
Field: verror.Field(),
Error: verror.Translate(lang),
}
fields = append(fields, field)
}
return &Error{
Err: errors.New("field validation error"),
Status: http.StatusBadRequest,
Fields: fields,
}, true
}
// IsShutdown checks to see if the shutdown error is contained
// in the specified error value.
func IsShutdown(err error) bool {
if _, ok := errors.Cause(err).(*shutdown); ok {
return true
}
return false
}

View File

@ -1,20 +1,18 @@
package web package web
import ( import (
"context"
"encoding/json" "encoding/json"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/weberror"
"net" "net"
"net/http" "net/http"
"reflect"
"strings" "strings"
"github.com/go-playground/locales/en"
ut "github.com/go-playground/universal-translator"
"github.com/gorilla/schema" "github.com/gorilla/schema"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/xwb1989/sqlparser" "github.com/xwb1989/sqlparser"
"github.com/xwb1989/sqlparser/dependency/querypb" "github.com/xwb1989/sqlparser/dependency/querypb"
"gopkg.in/go-playground/validator.v9"
en_translations "gopkg.in/go-playground/validator.v9/translations/en"
) )
// Headers // Headers
@ -33,74 +31,28 @@ const (
HeaderOrigin = "Origin" HeaderOrigin = "Origin"
) )
// validate holds the settings and caches for validating request struct values.
var validate = validator.New()
// translator is a cache of locale and translation information.
var translator *ut.UniversalTranslator
func init() {
// Instantiate the english locale for the validator library.
enLocale := en.New()
// Create a value using English as the fallback locale (first argument).
// Provide one or more arguments for additional supported locales.
translator = ut.New(enLocale, enLocale)
// Register the english error messages for validation errors.
lang, _ := translator.GetTranslator("en")
en_translations.RegisterDefaultTranslations(validate, lang)
// Use JSON tag names for errors instead of Go struct names.
validate = NewValidator()
// Empty method that can be overwritten in business logic packages to prevent web.Decode from failing.
f := func(fl validator.FieldLevel) bool {
return true
}
validate.RegisterValidation("unique", f)
}
// NewValidator inits a new validator with custom settings.
func NewValidator() *validator.Validate {
var v = validator.New()
// Use JSON tag names for errors instead of Go struct names.
v.RegisterTagNameFunc(func(fld reflect.StructField) string {
name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
if name == "-" {
return ""
}
return name
})
return v
}
// Decode reads the body of an HTTP request looking for a JSON document. The // Decode reads the body of an HTTP request looking for a JSON document. The
// body is decoded into the provided value. // body is decoded into the provided value.
// //
// If the provided value is a struct then it is checked for validation tags. // If the provided value is a struct then it is checked for validation tags.
func Decode(r *http.Request, val interface{}) error { func Decode(ctx context.Context, r *http.Request, val interface{}) error {
if r.Method == http.MethodPost || r.Method == http.MethodPut || r.Method == http.MethodPatch || r.Method == http.MethodDelete { if r.Method == http.MethodPost || r.Method == http.MethodPut || r.Method == http.MethodPatch || r.Method == http.MethodDelete {
decoder := json.NewDecoder(r.Body) decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields() decoder.DisallowUnknownFields()
if err := decoder.Decode(val); err != nil { if err := decoder.Decode(val); err != nil {
err = errors.Wrap(err, "decode request body failed") return weberror.NewErrorMessage(ctx, err, http.StatusBadRequest, "decode request body failed")
return NewRequestError(err, http.StatusBadRequest)
} }
} else { } else {
decoder := schema.NewDecoder() decoder := schema.NewDecoder()
if err := decoder.Decode(val, r.URL.Query()); err != nil { if err := decoder.Decode(val, r.URL.Query()); err != nil {
err = errors.Wrap(err, "decode request query failed") err = errors.Wrap(err, "decode request query failed")
return NewRequestError(err, http.StatusBadRequest) return weberror.NewErrorMessage(ctx, err, http.StatusBadRequest, "decode request query failed")
} }
} }
if err := validate.Struct(val); err != nil { if err := webcontext.Validator().Struct(val); err != nil {
verr, _ := NewValidationError(err) verr, _ := weberror.NewValidationError(ctx, err)
return verr return verr
} }

View File

@ -4,12 +4,15 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/pkg/errors" "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/weberror"
) )
const ( const (
@ -30,32 +33,20 @@ const (
// RespondJsonError sends an error formatted as JSON response back to the client. // RespondJsonError sends an error formatted as JSON response back to the client.
func RespondJsonError(ctx context.Context, w http.ResponseWriter, err error) error { func RespondJsonError(ctx context.Context, w http.ResponseWriter, err error) error {
// If the error was of the type *Error, the handler has // Set the status code for the request logger middleware.
// a specific status code and error to return. // If the context is missing this value, request the service
webErr, ok := errors.Cause(err).(*Error) // to be shutdown gracefully.
if !ok { v, err := webcontext.ContextValues(ctx)
webErr, ok = err.(*Error) if err != nil {
}
if ok {
er := ErrorResponse{
Error: webErr.Err.Error(),
Fields: webErr.Fields,
}
if err := RespondJson(ctx, w, er, webErr.Status); err != nil {
return err
}
return nil
}
// If not, the handler sent any arbitrary error value so use 500.
er := ErrorResponse{
Error: http.StatusText(http.StatusInternalServerError),
}
if err := RespondJson(ctx, w, er, http.StatusInternalServerError); err != nil {
return err return err
} }
return nil
// If the error was of the type *Error, the handler has
// a specific status code and error to return.
webErr := weberror.NewError(ctx, err, v.StatusCode).(*weberror.Error)
v.StatusCode = webErr.Status
return RespondJson(ctx, w, webErr.Display(ctx), 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.
@ -65,9 +56,9 @@ func RespondJson(ctx context.Context, w http.ResponseWriter, data interface{}, s
// Set the status code for the request logger middleware. // Set the status code for the request logger middleware.
// If the context is missing this value, request the service // If the context is missing this value, request the service
// to be shutdown gracefully. // to be shutdown gracefully.
v, ok := ctx.Value(KeyValues).(*Values) v, err := webcontext.ContextValues(ctx)
if !ok { if err != nil {
return NewShutdownError("web value missing from context") return err
} }
v.StatusCode = statusCode v.StatusCode = statusCode
@ -105,25 +96,39 @@ func RespondJson(ctx context.Context, w http.ResponseWriter, data interface{}, s
// RespondError sends an error back to the client as plain text with // RespondError sends an error back to the client as plain text with
// the status code 500 Internal Service Error // the status code 500 Internal Service Error
func RespondError(ctx context.Context, w http.ResponseWriter, er error) error { func RespondError(ctx context.Context, w http.ResponseWriter, er error) error {
return RespondErrorStatus(ctx, w, er, http.StatusInternalServerError) return RespondErrorStatus(ctx, w, er, 0)
} }
// RespondErrorStatus sends an error back to the client as plain text with // RespondErrorStatus sends an error back to the client as plain text with
// the specified HTTP status code. // the specified HTTP status code.
func RespondErrorStatus(ctx context.Context, w http.ResponseWriter, er error, statusCode int) error { func RespondErrorStatus(ctx context.Context, w http.ResponseWriter, er error, statusCode int) error {
msg := fmt.Sprintf("%s", er)
if err := Respond(ctx, w, []byte(msg), statusCode, MIMETextPlainCharsetUTF8); err != nil { // Set the status code for the request logger middleware.
// If the context is missing this value, request the service
// to be shutdown gracefully.
v, err := webcontext.ContextValues(ctx)
if err != nil {
return err return err
} }
return nil
// If the error was of the type *Error, the handler has
// a specific status code and error to return.
webErr := weberror.NewError(ctx, er, v.StatusCode).(*weberror.Error)
v.StatusCode = webErr.Status
respErr := webErr.Display(ctx).String()
switch webcontext.ContextEnv(ctx) {
case webcontext.Env_Dev, webcontext.Env_Stage:
respErr = respErr + fmt.Sprintf("\n%s\n%+v", webErr.Error(), webErr.Cause)
}
return RespondText(ctx, w, respErr, statusCode)
} }
// RespondText sends text back to the client as plain text with the specified HTTP status code. // RespondText sends text back to the client as plain text with the specified HTTP status code.
func RespondText(ctx context.Context, w http.ResponseWriter, text string, statusCode int) error { func RespondText(ctx context.Context, w http.ResponseWriter, text string, statusCode int) error {
if err := Respond(ctx, w, []byte(text), statusCode, MIMETextPlainCharsetUTF8); err != nil { return Respond(ctx, w, []byte(text), statusCode, MIMETextPlainCharsetUTF8)
return err
}
return nil
} }
// Respond writes the data to the client with the specified HTTP status code and // Respond writes the data to the client with the specified HTTP status code and
@ -133,9 +138,9 @@ func Respond(ctx context.Context, w http.ResponseWriter, data []byte, statusCode
// Set the status code for the request logger middleware. // Set the status code for the request logger middleware.
// If the context is missing this value, request the service // If the context is missing this value, request the service
// to be shutdown gracefully. // to be shutdown gracefully.
v, ok := ctx.Value(KeyValues).(*Values) v, err := webcontext.ContextValues(ctx)
if !ok { if err != nil {
return NewShutdownError("web value missing from context") return err
} }
v.StatusCode = statusCode v.StatusCode = statusCode
@ -159,6 +164,46 @@ func Respond(ctx context.Context, w http.ResponseWriter, data []byte, statusCode
return nil return nil
} }
// RenderError sends an error back to the client as html with
// the specified HTTP status code.
func RenderError(ctx context.Context, w http.ResponseWriter, r *http.Request, er error, renderer Renderer, templateLayoutName, templateContentName, contentType string) error {
// Set the status code for the request logger middleware.
// If the context is missing this value, request the service
// to be shutdown gracefully.
v, err := webcontext.ContextValues(ctx)
if err != nil {
return err
}
// If the error was of the type *Error, the handler has
// a specific status code and error to return.
webErr := weberror.NewError(ctx, er, v.StatusCode).(*weberror.Error)
v.StatusCode = webErr.Status
respErr := webErr.Display(ctx)
var fullError string
switch webcontext.ContextEnv(ctx) {
case webcontext.Env_Dev, webcontext.Env_Stage:
if webErr.Cause != nil && webErr.Cause.Error() != webErr.Err.Error() {
fullError = fmt.Sprintf("\n%s\n%+v", webErr.Error(), webErr.Cause)
} else {
fullError = fmt.Sprintf("%+v", webErr.Err)
}
fullError = strings.Replace(fullError, "\n", "<br/>", -1)
}
data := map[string]interface{}{
"statusCode": webErr.Status,
"errorMessage": respErr.Error,
"fullError": template.HTML(fullError),
}
return renderer.Render(ctx, w, r, templateLayoutName, templateContentName, contentType, webErr.Status, 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
// provided root directory. All errors will result in 404 File Not Found. // provided root directory. All errors will result in 404 File Not Found.
func Static(rootDir, prefix string) Handler { func Static(rootDir, prefix string) Handler {

View File

@ -3,12 +3,15 @@ package template_renderer
import ( import (
"context" "context"
"fmt" "fmt"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/auth"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/weberror"
"html/template" "html/template"
"math" "math"
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
"path/filepath" "path/filepath"
"reflect"
"strings" "strings"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web" "geeks-accelerator/oss/saas-starter-kit/internal/platform/web"
@ -105,6 +108,49 @@ func NewTemplate(templateFuncs template.FuncMap) *Template {
"html": func(value interface{}) template.HTML { "html": func(value interface{}) template.HTML {
return template.HTML(fmt.Sprint(value)) return template.HTML(fmt.Sprint(value))
}, },
"HasAuth": func(ctx context.Context) bool {
claims, err := auth.ClaimsFromContext(ctx)
if err != nil {
return false
}
return claims.HasAuth()
},
"HasRole": func(ctx context.Context, roles ...string) bool {
claims, err := auth.ClaimsFromContext(ctx)
if err != nil {
return false
}
return claims.HasRole(roles...)
},
"dict": func(values ...interface{}) (map[string]interface{}, error) {
if len(values) == 0 {
return nil, errors.New("invalid dict call")
}
dict := make(map[string]interface{})
for i := 0; i < len(values); i++ {
key, isset := values[i].(string)
if !isset {
if reflect.TypeOf(values[i]).Kind() == reflect.Map {
m := values[i].(map[string]interface{})
for i, v := range m {
dict[i] = v
}
} else {
return nil, errors.New("dict values must be maps")
}
} else {
i++
if i == len(values) {
return nil, errors.New("specify the key for non array values")
}
dict[key] = values[i]
}
}
return dict, nil
},
} }
for fn, f := range templateFuncs { for fn, f := range templateFuncs {
t.Funcs[fn] = f t.Funcs[fn] = f
@ -280,6 +326,24 @@ func (r *TemplateRenderer) Render(ctx context.Context, w http.ResponseWriter, re
// to define context.Context as an argument // to define context.Context as an argument
renderData["_Ctx"] = ctx renderData["_Ctx"] = ctx
if qv := req.URL.Query().Get("test-validation-error"); qv != "" {
data["validationErrors"] = data["validationDefaults"]
}
if qv := req.URL.Query().Get("test-web-error"); qv != "" {
terr := errors.New("Some random error")
terr = errors.WithMessage(terr, "Actual error message")
rerr := weberror.NewError(ctx, terr, http.StatusBadRequest).(*weberror.Error)
rerr.Message = "Test Web Error Message"
data["error"] = rerr
}
if qv := req.URL.Query().Get("test-error"); qv != "" {
terr := errors.New("Test error")
terr = errors.WithMessage(terr, "Error message")
data["error"] = terr
}
// Append request data map to render data last so any previous value can be overwritten. // Append request data map to render data last so any previous value can be overwritten.
if data != nil { if data != nil {
for k, v := range data { for k, v := range data {

View File

@ -2,6 +2,8 @@ package web
import ( import (
"context" "context"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/weberror"
"log" "log"
"net/http" "net/http"
"os" "os"
@ -11,20 +13,6 @@ import (
"github.com/dimfeld/httptreemux" "github.com/dimfeld/httptreemux"
) )
// ctxKey represents the type of value for the context key.
type ctxKey int
// KeyValues is how request values or stored/retrieved.
const KeyValues ctxKey = 1
// Values represent state for each request.
type Values struct {
Now time.Time
TraceID uint64
SpanID uint64
StatusCode int
}
// A Handler is a type that handles an http request within our own little mini // A Handler is a type that handles an http request within our own little mini
// framework. // framework.
type Handler func(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error type Handler func(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error
@ -36,15 +24,17 @@ type App struct {
*httptreemux.TreeMux *httptreemux.TreeMux
shutdown chan os.Signal shutdown chan os.Signal
log *log.Logger log *log.Logger
env webcontext.Env
mw []Middleware mw []Middleware
} }
// NewApp creates an App value that handle a set of routes for the application. // NewApp creates an App value that handle a set of routes for the application.
func NewApp(shutdown chan os.Signal, log *log.Logger, mw ...Middleware) *App { func NewApp(shutdown chan os.Signal, log *log.Logger, env webcontext.Env, mw ...Middleware) *App {
app := App{ app := App{
TreeMux: httptreemux.New(), TreeMux: httptreemux.New(),
shutdown: shutdown, shutdown: shutdown,
log: log, log: log,
env: env,
mw: mw, mw: mw,
} }
@ -76,17 +66,18 @@ func (a *App) Handle(verb, path string, handler Handler, mw ...Middleware) {
h := func(w http.ResponseWriter, r *http.Request, params map[string]string) { h := func(w http.ResponseWriter, r *http.Request, params map[string]string) {
// Set the context with the required values to // Set the context with the required values to
// process the request. // process the request.
v := Values{ v := webcontext.Values{
Now: time.Now(), Now: time.Now(),
Env: a.env,
} }
ctx := context.WithValue(r.Context(), KeyValues, &v) ctx := context.WithValue(r.Context(), webcontext.KeyValues, &v)
// Call the wrapped handler functions. // Call the wrapped handler functions.
err := handler(ctx, w, r, params) err := handler(ctx, w, r, params)
if err != nil { if err != nil {
// If we have specifically handled the error, then no need // If we have specifically handled the error, then no need
// to initiate a shutdown. // to initiate a shutdown.
if webErr, ok := err.(*Error); ok { if webErr, ok := err.(*weberror.Error); ok {
// Render an error response. // Render an error response.
if rerr := RespondErrorStatus(ctx, w, webErr.Err, webErr.Status); rerr == nil { if rerr := RespondErrorStatus(ctx, w, webErr.Err, webErr.Status); rerr == nil {
// If there was not error rending the error, then no need to continue. // If there was not error rending the error, then no need to continue.

View File

@ -0,0 +1,53 @@
package webcontext
import (
"context"
"time"
"github.com/pkg/errors"
)
// ctxKey represents the type of value for the context key.
type ctxKey int
// KeyValues is how request values or stored/retrieved.
const KeyValues ctxKey = 1
var ErrContextRequired = errors.New("web value missing from context")
// Values represent state for each request.
type Values struct {
Now time.Time
TraceID uint64
SpanID uint64
StatusCode int
Env Env
}
func ContextValues(ctx context.Context) (*Values, error) {
// If the context is missing this value, request the service
// to be shutdown gracefully.
v, ok := ctx.Value(KeyValues).(*Values)
if !ok {
e := Values{}
return &e, ErrContextRequired
}
return v, nil
}
type Env = string
var (
Env_Dev Env = "dev"
Env_Stage Env = "stage"
Env_Prod Env = "prod"
)
func ContextEnv(ctx context.Context) string {
cv := ctx.Value(KeyValues).(*Values)
if cv != nil {
return cv.Env
}
return ""
}

View File

@ -0,0 +1,157 @@
package webcontext
import (
"context"
"reflect"
"strings"
"github.com/go-playground/locales/en"
"github.com/go-playground/locales/fr"
"github.com/go-playground/locales/id"
"github.com/go-playground/locales/ja"
"github.com/go-playground/locales/nl"
"github.com/go-playground/locales/zh"
ut "github.com/go-playground/universal-translator"
"gopkg.in/go-playground/validator.v9"
en_translations "gopkg.in/go-playground/validator.v9/translations/en"
fr_translations "gopkg.in/go-playground/validator.v9/translations/fr"
id_translations "gopkg.in/go-playground/validator.v9/translations/id"
ja_translations "gopkg.in/go-playground/validator.v9/translations/ja"
nl_translations "gopkg.in/go-playground/validator.v9/translations/nl"
zh_translations "gopkg.in/go-playground/validator.v9/translations/zh"
)
// ctxKeyTranslate represents the type of value for the context key.
type ctxKeyTranslate int
// KeyTranslate is used to store/retrieve a Claims value from a context.Context.
const KeyTranslate ctxKeyTranslate = 1
// ContextWithTranslator appends a universal translator to a context.
func ContextWithTranslator(ctx context.Context, translator ut.Translator) context.Context {
return context.WithValue(ctx, KeyTranslate, translator)
}
// ContextTranslator returns the universal context from a context.
func ContextTranslator(ctx context.Context) ut.Translator {
return ctx.Value(KeyTranslate).(ut.Translator)
}
// validate holds the settings and caches for validating request struct values.
var validate *validator.Validate
// translator is a cache of locale and translation information.
var uniTrans *ut.UniversalTranslator
func init() {
// Example from https://github.com/go-playground/universal-translator/issues/7
// Instantiate the english and french locale for the validator library.
en := en.New()
fr := fr.New()
id := id.New()
ja := ja.New()
nl := nl.New()
zh := zh.New()
// Create a value using English as the fallback locale (first argument).
// Provide one or more arguments for additional supported locales.
uniTrans = ut.New(en, en, fr, id, ja, nl, zh)
// this is usually know or extracted from http 'Accept-Language' header
// also see uni.FindTranslator(...)
transEn, _ := uniTrans.GetTranslator(en.Locale())
transFr, _ := uniTrans.GetTranslator(fr.Locale())
transId, _ := uniTrans.GetTranslator(id.Locale())
transJa, _ := uniTrans.GetTranslator(ja.Locale())
transNl, _ := uniTrans.GetTranslator(nl.Locale())
transZh, _ := uniTrans.GetTranslator(zh.Locale())
transEn.Add("{{name}}", "Name", false)
transFr.Add("{{name}}", "Nom", false)
transEn.Add("{{first_name}}", "First Name", false)
transFr.Add("{{first_name}}", "Prénom", false)
transEn.Add("{{last_name}}", "Last Name", false)
transFr.Add("{{last_name}}", "Nom de famille", false)
validate = newValidator()
en_translations.RegisterDefaultTranslations(validate, transEn)
fr_translations.RegisterDefaultTranslations(validate, transFr)
id_translations.RegisterDefaultTranslations(validate, transId)
ja_translations.RegisterDefaultTranslations(validate, transJa)
nl_translations.RegisterDefaultTranslations(validate, transNl)
zh_translations.RegisterDefaultTranslations(validate, transZh)
/*
validate.RegisterTranslation("required", transEn, func(ut ut.Translator) error {
return ut.Add("required", "{0} must have a value!", true) // see universal-translator for details
}, func(ut ut.Translator, fe validator.FieldError) string {
t, _ := ut.T("required", fe.Field())
return t
})
validate.RegisterTranslation("required", transFr, func(ut ut.Translator) error {
return ut.Add("required", "{0} must have a value!", true) // see universal-translator for details
}, func(ut ut.Translator, fe validator.FieldError) string {
t, _ := ut.T("required", fe.Field())
return t
})
validate.RegisterTranslation("unique", transEn, func(ut ut.Translator) error {
return ut.Add("unique", "{0} must be unique", true) // see universal-translator for details
}, func(ut ut.Translator, fe validator.FieldError) string {
t, _ := ut.T("unique", fe.Field())
return t
})
validate.RegisterTranslation("unique", transFr, func(ut ut.Translator) error {
return ut.Add("unique", "{0} must be unique", true) // see universal-translator for details
}, func(ut ut.Translator, fe validator.FieldError) string {
t, _ := ut.T("unique", fe.Field())
return t
})*/
}
// newValidator inits a new validator with custom settings.
func newValidator() *validator.Validate {
var v = validator.New()
// Use JSON tag names for errors instead of Go struct names.
v.RegisterTagNameFunc(func(fld reflect.StructField) string {
name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
if name == "-" {
return ""
}
return "{{" + name + "}}"
})
// Empty method that can be overwritten in business logic packages to prevent web.Decode from failing.
f := func(fl validator.FieldLevel) bool {
return true
}
v.RegisterValidation("unique", f)
return v
}
// Validator returns the current init validator.
func Validator() *validator.Validate {
return validate
}
// UniversalTranslator returns the current UniversalTranslator.
func UniversalTranslator() *ut.UniversalTranslator {
return uniTrans
}

View File

@ -0,0 +1,121 @@
package weberror
import (
"context"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext"
"net/http"
"github.com/pkg/errors"
)
// Error is used to pass an error during the request through the
// application with web specific context.
type Error struct {
Err error
Status int
Fields []FieldError
Cause error
Message string
}
// FieldError is used to indicate an error with a specific request field.
type FieldError struct {
Field string `json:"field"`
FormField string `json:"-"`
Value interface{} `json:"value"`
Tag string `json:"tag"`
Error string `json:"error"`
Display string `json:"display"`
}
// NewError wraps a provided error with an HTTP status code. This
// function should be used when handlers encounter expected errors.
func NewError(ctx context.Context, er error, status int) error {
webErr, ok := er.(*Error)
if ok {
return webErr
}
// If the error was of the type *Error, the handler has
// a specific status code and error to return.
webErr, ok = errors.Cause(er).(*Error)
if ok {
return webErr
}
// Ensure the error is not a validation error.
if ne, ok := NewValidationError(ctx, er); ok {
return ne
}
if er == webcontext.ErrContextRequired {
return NewShutdownError(er.Error())
}
// If not, the handler sent any arbitrary error value so use 500.
if status == 0 {
status = http.StatusInternalServerError
}
cause := errors.Cause(er)
if cause == nil {
cause = er
}
return &Error{er, status, nil, cause, ""}
}
// Error implements the error interface. It uses the default message of the
// wrapped error. This is what will be shown in the services' logs.
func (err *Error) Error() string {
return err.Err.Error()
}
// Display renders an error that can be returned as ErrorResponse to the user via the API.
func (er *Error) Display(ctx context.Context) ErrorResponse {
var r ErrorResponse
if er.Message != "" {
r.Error = er.Message
} else {
r.Error = er.Error()
}
if len(er.Fields) > 0 {
r.Fields = er.Fields
}
return r
}
// ErrorResponse is the form used for API responses from failures in the API.
type ErrorResponse struct {
Error string `json:"error"`
Fields []FieldError `json:"fields,omitempty"`
}
// String returns the ErrorResponse formatted as a string.
func (er ErrorResponse) String() string {
str := er.Error
if len(er.Fields) > 0 {
for _, f := range er.Fields {
str = str + "\t" + f.Error + "\n"
}
}
return str
}
// NewErrorMessage wraps a provided error with an HTTP status code and message. The
// message value is given priority and returned as the error message.
func NewErrorMessage(ctx context.Context, er error, status int, msg string) error {
return WithMessage(ctx, NewError(ctx, er, status), msg)
}
// WithMessage appends the error with a message.
func WithMessage(ctx context.Context, er error, msg string) error {
weberr := NewError(ctx, er, 0).(*Error)
weberr.Message = msg
return weberr
}

View File

@ -0,0 +1,28 @@
package weberror
import "github.com/pkg/errors"
// shutdown is a type used to help with the graceful termination of the service.
type shutdown struct {
Message string
}
// Error is the implementation of the error interface.
func (s *shutdown) Error() string {
return s.Message
}
// NewShutdownError returns an error that causes the framework to signal
// a graceful shutdown.
func NewShutdownError(message string) error {
return &shutdown{message}
}
// IsShutdown checks to see if the shutdown error is contained
// in the specified error value.
func IsShutdown(err error) bool {
if _, ok := errors.Cause(err).(*shutdown); ok {
return true
}
return false
}

View File

@ -0,0 +1,103 @@
package weberror
import (
"context"
"fmt"
"net/http"
"strings"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext"
"github.com/iancoleman/strcase"
"github.com/pkg/errors"
"gopkg.in/go-playground/validator.v9"
)
// NewValidationError checks the error for validation errors and formats the correct response.
func NewValidationError(ctx context.Context, err error) (error, bool) {
// Use a type assertion to get the real error value.
verrors, ok := errors.Cause(err).(validator.ValidationErrors)
if !ok {
return err, false
}
// Load the translator from the context that will be set by query string or HTTP headers.
trans := webcontext.ContextTranslator(ctx)
//fields := make(map[string][]FieldError)
var fields []FieldError
for _, verror := range verrors {
jsonKey := verror.Field()
fieldName := jsonKey[2 : len(jsonKey)-2]
localName, _ := trans.T(jsonKey)
if localName == "" {
localName = fieldName
}
namespace := strings.Replace(verror.Namespace(), "{{", "", -1)
namespace = strings.Replace(namespace, "}}", "", -1)
fieldErr := strings.Replace(verror.Translate(trans), jsonKey, localName, -1)
fieldErr = strings.Replace(fieldErr, "{{", "", -1)
fieldErr = strings.Replace(fieldErr, "}}", "", -1)
field := FieldError{
Field: verror.Field(),
Value: verror.Value(),
Tag: verror.Tag(),
Error: fieldErr,
FormField: FormField(verror.StructNamespace()),
Display: fieldErr,
}
switch verror.Tag() {
case "required":
field.Display = fmt.Sprintf("%s is required.", localName)
case "unique":
field.Display = fmt.Sprintf("%s must be unique.", localName)
}
/*
fmt.Println("field", field.Error)
fmt.Println("formField", field.FormField)
fmt.Println("Namespace: " + verror.Namespace())
fmt.Println("Field: " + verror.Field())
fmt.Println("StructNamespace: " + verror.StructNamespace()) // can differ when a custom TagNameFunc is registered or
fmt.Println("StructField: " + verror.StructField()) // by passing alt name to ReportError like below
fmt.Println("Tag: " + verror.Tag())
fmt.Println("ActualTag: " + verror.ActualTag())
fmt.Println("Kind: ", verror.Kind())
fmt.Println("Type: ", verror.Type())
fmt.Println("Value: ", verror.Value())
fmt.Println("Param: " + verror.Param())
fmt.Println()
*/
fields = append(fields, field)
}
return &Error{
Err: err,
Status: http.StatusBadRequest,
Fields: fields,
Cause: err,
Message: "Field validation error",
}, true
}
func FormField(namespace string) string {
if !strings.Contains(namespace, ".") {
return namespace
}
pts := strings.Split(namespace, ".")
var newPts []string
for i := 1; i < len(pts); i++ {
newPts = append(newPts, strcase.ToCamel(pts[i]))
}
return strings.Join(newPts, ".")
}

View File

@ -4,7 +4,7 @@ import (
"context" "context"
"database/sql" "database/sql"
"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" "geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext"
"github.com/huandu/go-sqlbuilder" "github.com/huandu/go-sqlbuilder"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
"github.com/pborman/uuid" "github.com/pborman/uuid"
@ -231,7 +231,7 @@ func Create(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req Projec
} }
// Validate the request. // Validate the request.
v := web.NewValidator() v := webcontext.Validator()
err := v.Struct(req) err := v.Struct(req)
if err != nil { if err != nil {
return nil, err return nil, err
@ -302,7 +302,7 @@ func Update(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req Projec
defer span.Finish() defer span.Finish()
// Validate the request. // Validate the request.
v := web.NewValidator() v := webcontext.Validator()
err := v.Struct(req) err := v.Struct(req)
if err != nil { if err != nil {
return err return err
@ -372,7 +372,7 @@ func Archive(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req Proje
defer span.Finish() defer span.Finish()
// Validate the request. // Validate the request.
v := web.NewValidator() v := webcontext.Validator()
err := v.Struct(req) err := v.Struct(req)
if err != nil { if err != nil {
return err return err
@ -428,7 +428,7 @@ func Delete(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, id string)
} }
// Validate the request. // Validate the request.
v := web.NewValidator() v := webcontext.Validator()
err := v.Struct(req) err := v.Struct(req)
if err != nil { if err != nil {
return err return err

View File

@ -14,7 +14,7 @@ import (
// migration already exists in the migrations table it will be skipped. // migration already exists in the migrations table it will be skipped.
func migrationList(db *sqlx.DB, log *log.Logger) []*sqlxmigrate.Migration { func migrationList(db *sqlx.DB, log *log.Logger) []*sqlxmigrate.Migration {
return []*sqlxmigrate.Migration{ return []*sqlxmigrate.Migration{
// create table users // Create table users.
{ {
ID: "20190522-01a", ID: "20190522-01a",
Migrate: func(tx *sql.Tx) error { Migrate: func(tx *sql.Tx) error {
@ -45,7 +45,7 @@ func migrationList(db *sqlx.DB, log *log.Logger) []*sqlxmigrate.Migration {
return nil return nil
}, },
}, },
// create new table accounts // Create new table accounts.
{ {
ID: "20190522-01b", ID: "20190522-01b",
Migrate: func(tx *sql.Tx) error { Migrate: func(tx *sql.Tx) error {
@ -91,7 +91,7 @@ func migrationList(db *sqlx.DB, log *log.Logger) []*sqlxmigrate.Migration {
return nil return nil
}, },
}, },
// create new table user_accounts // Create new table user_accounts.
{ {
ID: "20190522-01d", ID: "20190522-01d",
Migrate: func(tx *sql.Tx) error { Migrate: func(tx *sql.Tx) error {
@ -142,7 +142,7 @@ func migrationList(db *sqlx.DB, log *log.Logger) []*sqlxmigrate.Migration {
return nil return nil
}, },
}, },
// create new table projects // Create new table projects.
{ {
ID: "20190622-01", ID: "20190622-01",
Migrate: func(tx *sql.Tx) error { Migrate: func(tx *sql.Tx) error {
@ -179,5 +179,31 @@ func migrationList(db *sqlx.DB, log *log.Logger) []*sqlxmigrate.Migration {
return nil return nil
}, },
}, },
// Split users.name into first_name and last_name columns.
{
ID: "201907-29-01a",
Migrate: func(tx *sql.Tx) error {
q1 := `ALTER TABLE users
RENAME COLUMN name to first_name;`
if _, err := tx.Exec(q1); err != nil {
return errors.WithMessagef(err, "Query failed %s", q1)
}
q2 := `ALTER TABLE users
ADD last_name varchar(200) NOT NULL DEFAULT '';`
if _, err := tx.Exec(q2); err != nil {
return errors.WithMessagef(err, "Query failed %s", q2)
}
return nil
},
Rollback: func(tx *sql.Tx) error {
q1 := `DROP TABLE IF EXISTS users`
if _, err := tx.Exec(q1); err != nil {
return errors.WithMessagef(err, "Query failed %s", q1)
}
return nil
},
},
} }
} }

View File

@ -26,10 +26,11 @@ type SignupAccount struct {
// SignupUser defined the details needed for user. // SignupUser defined the details needed for user.
type SignupUser struct { type SignupUser struct {
Name string `json:"name" validate:"required" example:"Gabi May"` FirstName string `json:"first_name" validate:"required" example:"Gabi"`
LastName string `json:"last_name" validate:"required" example:"May"`
Email string `json:"email" validate:"required,email,unique" example:"{RANDOM_EMAIL}"` Email string `json:"email" validate:"required,email,unique" example:"{RANDOM_EMAIL}"`
Password string `json:"password" validate:"required" example:"SecretString"` Password string `json:"password" validate:"required" example:"SecretString"`
PasswordConfirm string `json:"password_confirm" validate:"eqfield=Password" example:"SecretString"` PasswordConfirm string `json:"password_confirm" validate:"required,eqfield=Password" example:"SecretString"`
} }
// SignupResult response signup with created account and user. // SignupResult response signup with created account and user.

View File

@ -2,16 +2,15 @@ package signup
import ( import (
"context" "context"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web"
"time"
"geeks-accelerator/oss/saas-starter-kit/internal/account" "geeks-accelerator/oss/saas-starter-kit/internal/account"
"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/user" "geeks-accelerator/oss/saas-starter-kit/internal/user"
"geeks-accelerator/oss/saas-starter-kit/internal/user_account" "geeks-accelerator/oss/saas-starter-kit/internal/user_account"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
"gopkg.in/go-playground/validator.v9" "gopkg.in/go-playground/validator.v9"
"time"
) )
// Signup performs the steps needed to create a new account, new user and then associate // Signup performs the steps needed to create a new account, new user and then associate
@ -48,7 +47,7 @@ func Signup(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req Signup
return uniq return uniq
} }
v := web.NewValidator() v := webcontext.Validator()
v.RegisterValidation("unique", f) v.RegisterValidation("unique", f)
// Validate the request. // Validate the request.
@ -61,7 +60,8 @@ func Signup(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req Signup
// UserCreateRequest contains information needed to create a new User. // UserCreateRequest contains information needed to create a new User.
userReq := user.UserCreateRequest{ userReq := user.UserCreateRequest{
Name: req.User.Name, FirstName: req.User.FirstName,
LastName: req.User.LastName,
Email: req.User.Email, Email: req.User.Email,
Password: req.User.Password, Password: req.User.Password,
PasswordConfirm: req.User.PasswordConfirm, PasswordConfirm: req.User.PasswordConfirm,

View File

@ -4,7 +4,7 @@ import (
"context" "context"
"crypto/rsa" "crypto/rsa"
"database/sql" "database/sql"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web" "geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext"
"github.com/dgrijalva/jwt-go" "github.com/dgrijalva/jwt-go"
"strings" "strings"
"time" "time"
@ -80,7 +80,7 @@ func SwitchAccount(ctx context.Context, dbConn *sqlx.DB, tknGen TokenGenerator,
} }
// Validate the request. // Validate the request.
v := web.NewValidator() v := webcontext.Validator()
err := v.Struct(req) err := v.Struct(req)
if err != nil { if err != nil {
return Token{}, err return Token{}, err

View File

@ -40,7 +40,8 @@ func TestAuthenticate(t *testing.T) {
// Create a new user for testing. // Create a new user for testing.
initPass := uuid.NewRandom().String() initPass := uuid.NewRandom().String()
user, err := Create(ctx, auth.Claims{}, test.MasterDB, UserCreateRequest{ user, err := Create(ctx, auth.Claims{}, test.MasterDB, UserCreateRequest{
Name: "Lee Brown", FirstName: "Lee",
LastName: "Brown",
Email: uuid.NewRandom().String() + "@geeksinthewoods.com", Email: uuid.NewRandom().String() + "@geeksinthewoods.com",
Password: initPass, Password: initPass,
PasswordConfirm: initPass, PasswordConfirm: initPass,

View File

@ -13,7 +13,8 @@ import (
// User represents someone with access to our system. // User represents someone with access to our system.
type User struct { type User struct {
ID string `json:"id" validate:"required,uuid" example:"d69bdef7-173f-4d29-b52c-3edc60baf6a2"` ID string `json:"id" validate:"required,uuid" example:"d69bdef7-173f-4d29-b52c-3edc60baf6a2"`
Name string `json:"name" validate:"required" example:"Gabi May"` FirstName string `json:"first_name" validate:"required" example:"Gabi"`
LastName string `json:"last_name" validate:"required" example:"May"`
Email string `json:"email" validate:"required,email,unique" example:"gabi@geeksinthewoods.com"` Email string `json:"email" validate:"required,email,unique" example:"gabi@geeksinthewoods.com"`
PasswordSalt string `json:"-" validate:"required"` PasswordSalt string `json:"-" validate:"required"`
PasswordHash []byte `json:"-" validate:"required"` PasswordHash []byte `json:"-" validate:"required"`
@ -27,7 +28,8 @@ type User struct {
// UserResponse represents someone with access to our system that is returned for display. // UserResponse represents someone with access to our system that is returned for display.
type UserResponse struct { type UserResponse struct {
ID string `json:"id" example:"d69bdef7-173f-4d29-b52c-3edc60baf6a2"` ID string `json:"id" example:"d69bdef7-173f-4d29-b52c-3edc60baf6a2"`
Name string `json:"name" example:"Gabi May"` FirstName string `json:"first_name" example:"Gabi"`
LastName string `json:"last_name" example:"May"`
Email string `json:"email" example:"gabi@geeksinthewoods.com"` Email string `json:"email" example:"gabi@geeksinthewoods.com"`
Timezone string `json:"timezone" example:"America/Anchorage"` Timezone string `json:"timezone" example:"America/Anchorage"`
CreatedAt web.TimeResponse `json:"created_at"` // CreatedAt contains multiple format options for display. CreatedAt web.TimeResponse `json:"created_at"` // CreatedAt contains multiple format options for display.
@ -44,7 +46,8 @@ func (m *User) Response(ctx context.Context) *UserResponse {
r := &UserResponse{ r := &UserResponse{
ID: m.ID, ID: m.ID,
Name: m.Name, FirstName: m.FirstName,
LastName: m.LastName,
Email: m.Email, Email: m.Email,
Timezone: m.Timezone, Timezone: m.Timezone,
CreatedAt: web.NewTimeResponse(ctx, m.CreatedAt), CreatedAt: web.NewTimeResponse(ctx, m.CreatedAt),
@ -61,10 +64,11 @@ func (m *User) Response(ctx context.Context) *UserResponse {
// UserCreateRequest contains information needed to create a new User. // UserCreateRequest contains information needed to create a new User.
type UserCreateRequest struct { type UserCreateRequest struct {
Name string `json:"name" validate:"required" example:"Gabi May"` FirstName string `json:"first_name" validate:"required" example:"Gabi"`
LastName string `json:"last_name" validate:"required" example:"May"`
Email string `json:"email" validate:"required,email,unique" example:"gabi@geeksinthewoods.com"` Email string `json:"email" validate:"required,email,unique" example:"gabi@geeksinthewoods.com"`
Password string `json:"password" validate:"required" example:"SecretString"` Password string `json:"password" validate:"required" example:"SecretString"`
PasswordConfirm string `json:"password_confirm" validate:"eqfield=Password" example:"SecretString"` PasswordConfirm string `json:"password_confirm" validate:"required,eqfield=Password" example:"SecretString"`
Timezone *string `json:"timezone,omitempty" validate:"omitempty" example:"America/Anchorage"` Timezone *string `json:"timezone,omitempty" validate:"omitempty" example:"America/Anchorage"`
} }
@ -75,17 +79,18 @@ type UserCreateRequest struct {
// we do not want to use pointers to basic types but we make exceptions around // we do not want to use pointers to basic types but we make exceptions around
// marshalling/unmarshalling. // marshalling/unmarshalling.
type UserUpdateRequest struct { type UserUpdateRequest struct {
ID string `json:"id" validate:"required,uuid" example:"d69bdef7-173f-4d29-b52c-3edc60baf6a2"` ID string `json:"id" validate:"required,uuid" example:"d69bdef7-173f-4d29-b52c-3edc60baf6a2"`
Name *string `json:"name,omitempty" validate:"omitempty" example:"Gabi May Not"` FirstName *string `json:"first_name,omitempty" validate:"omitempty" example:"Gabi May Not"`
Email *string `json:"email,omitempty" validate:"omitempty,email,unique" example:"gabi.may@geeksinthewoods.com"` LastName *string `json:"last_name,omitempty" validate:"omitempty" example:"Gabi May Not"`
Timezone *string `json:"timezone,omitempty" validate:"omitempty" example:"America/Anchorage"` Email *string `json:"email,omitempty" validate:"omitempty,email,unique" example:"gabi.may@geeksinthewoods.com"`
Timezone *string `json:"timezone,omitempty" validate:"omitempty" example:"America/Anchorage"`
} }
// UserUpdatePasswordRequest defines what information is required to update a user password. // UserUpdatePasswordRequest defines what information is required to update a user password.
type UserUpdatePasswordRequest struct { type UserUpdatePasswordRequest struct {
ID string `json:"id" validate:"required,uuid" example:"d69bdef7-173f-4d29-b52c-3edc60baf6a2"` ID string `json:"id" validate:"required,uuid" example:"d69bdef7-173f-4d29-b52c-3edc60baf6a2"`
Password string `json:"password" validate:"required" example:"NeverTellSecret"` Password string `json:"password" validate:"required" example:"NeverTellSecret"`
PasswordConfirm string `json:"password_confirm" validate:"omitempty,eqfield=Password" example:"NeverTellSecret"` PasswordConfirm string `json:"password_confirm" validate:"required,eqfield=Password" example:"NeverTellSecret"`
} }
// UserArchiveRequest defines the information needed to archive an user. This will archive (soft-delete) the // UserArchiveRequest defines the information needed to archive an user. This will archive (soft-delete) the

View File

@ -3,7 +3,7 @@ package user
import ( import (
"context" "context"
"database/sql" "database/sql"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web" "geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext"
"time" "time"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/auth" "geeks-accelerator/oss/saas-starter-kit/internal/platform/auth"
@ -38,7 +38,7 @@ var (
) )
// userMapColumns is the list of columns needed for mapRowsToUser // userMapColumns is the list of columns needed for mapRowsToUser
var userMapColumns = "id,name,email,password_salt,password_hash,password_reset,timezone,created_at,updated_at,archived_at" var userMapColumns = "id,first_name,last_name,email,password_salt,password_hash,password_reset,timezone,created_at,updated_at,archived_at"
// mapRowsToUser takes the SQL rows and maps it to the UserAccount struct // mapRowsToUser takes the SQL rows and maps it to the UserAccount struct
// with the columns defined by userMapColumns // with the columns defined by userMapColumns
@ -47,7 +47,7 @@ func mapRowsToUser(rows *sql.Rows) (*User, error) {
u User u User
err error err error
) )
err = rows.Scan(&u.ID, &u.Name, &u.Email, &u.PasswordSalt, &u.PasswordHash, &u.PasswordReset, &u.Timezone, &u.CreatedAt, &u.UpdatedAt, &u.ArchivedAt) err = rows.Scan(&u.ID, &u.FirstName, &u.LastName, &u.Email, &u.PasswordSalt, &u.PasswordHash, &u.PasswordReset, &u.Timezone, &u.CreatedAt, &u.UpdatedAt, &u.ArchivedAt)
if err != nil { if err != nil {
return nil, errors.WithStack(err) return nil, errors.WithStack(err)
} }
@ -290,7 +290,7 @@ func Create(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req UserCr
return uniq return uniq
} }
v := web.NewValidator() v := webcontext.Validator()
v.RegisterValidation("unique", f) v.RegisterValidation("unique", f)
// Validate the request. // Validate the request.
@ -331,7 +331,8 @@ func Create(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req UserCr
u := User{ u := User{
ID: uuid.NewRandom().String(), ID: uuid.NewRandom().String(),
Name: req.Name, FirstName: req.FirstName,
LastName: req.LastName,
Email: req.Email, Email: req.Email,
PasswordHash: passwordHash, PasswordHash: passwordHash,
PasswordSalt: passwordSalt, PasswordSalt: passwordSalt,
@ -347,8 +348,8 @@ func Create(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req UserCr
// Build the insert SQL statement. // Build the insert SQL statement.
query := sqlbuilder.NewInsertBuilder() query := sqlbuilder.NewInsertBuilder()
query.InsertInto(userTableName) query.InsertInto(userTableName)
query.Cols("id", "name", "email", "password_hash", "password_salt", "timezone", "created_at", "updated_at") query.Cols("id", "first_name", "last_name", "email", "password_hash", "password_salt", "timezone", "created_at", "updated_at")
query.Values(u.ID, u.Name, u.Email, u.PasswordHash, u.PasswordSalt, u.Timezone, u.CreatedAt, u.UpdatedAt) query.Values(u.ID, u.FirstName, u.LastName, u.Email, u.PasswordHash, u.PasswordSalt, u.Timezone, u.CreatedAt, u.UpdatedAt)
// Execute the query with the provided context. // Execute the query with the provided context.
sql, args := query.Build() sql, args := query.Build()
@ -389,7 +390,7 @@ func Update(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req UserUp
span, ctx := tracer.StartSpanFromContext(ctx, "internal.user.Update") span, ctx := tracer.StartSpanFromContext(ctx, "internal.user.Update")
defer span.Finish() defer span.Finish()
v := web.NewValidator() v := webcontext.Validator()
// Validation email address is unique in the database. // Validation email address is unique in the database.
if req.Email != nil { if req.Email != nil {
@ -435,8 +436,11 @@ func Update(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req UserUp
query.Update(userTableName) query.Update(userTableName)
var fields []string var fields []string
if req.Name != nil { if req.FirstName != nil {
fields = append(fields, query.Assign("name", req.Name)) fields = append(fields, query.Assign("name", req.FirstName))
}
if req.LastName != nil {
fields = append(fields, query.Assign("name", req.LastName))
} }
if req.Email != nil { if req.Email != nil {
fields = append(fields, query.Assign("email", req.Email)) fields = append(fields, query.Assign("email", req.Email))
@ -475,7 +479,7 @@ func UpdatePassword(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, re
defer span.Finish() defer span.Finish()
// Validate the request. // Validate the request.
v := web.NewValidator() v := webcontext.Validator()
err := v.Struct(req) err := v.Struct(req)
if err != nil { if err != nil {
return err return err
@ -544,7 +548,7 @@ func Archive(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req UserA
defer span.Finish() defer span.Finish()
// Validate the request. // Validate the request.
v := web.NewValidator() v := webcontext.Validator()
err := v.Struct(req) err := v.Struct(req)
if err != nil { if err != nil {
return err return err
@ -623,7 +627,7 @@ func Delete(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, userID str
} }
// Validate the request. // Validate the request.
v := web.NewValidator() v := webcontext.Validator()
err := v.Struct(req) err := v.Struct(req)
if err != nil { if err != nil {
return err return err

View File

@ -32,7 +32,7 @@ func testMain(m *testing.M) int {
// TestFindRequestQuery validates findRequestQuery // TestFindRequestQuery validates findRequestQuery
func TestFindRequestQuery(t *testing.T) { func TestFindRequestQuery(t *testing.T) {
where := "name = ? or email = ?" where := "first_name = ? or email = ?"
var ( var (
limit uint = 12 limit uint = 12
offset uint = 34 offset uint = 34
@ -41,7 +41,7 @@ func TestFindRequestQuery(t *testing.T) {
req := UserFindRequest{ req := UserFindRequest{
Where: &where, Where: &where,
Args: []interface{}{ Args: []interface{}{
"lee brown", "lee",
"lee@geeksinthewoods.com", "lee@geeksinthewoods.com",
}, },
Order: []string{ Order: []string{
@ -51,7 +51,7 @@ func TestFindRequestQuery(t *testing.T) {
Limit: &limit, Limit: &limit,
Offset: &offset, Offset: &offset,
} }
expected := "SELECT " + userMapColumns + " FROM " + userTableName + " WHERE (name = ? or email = ?) ORDER BY id asc, created_at desc LIMIT 12 OFFSET 34" expected := "SELECT " + userMapColumns + " FROM " + userTableName + " WHERE (first_name = ? or email = ?) ORDER BY id asc, created_at desc LIMIT 12 OFFSET 34"
res, args := findRequestQuery(req) res, args := findRequestQuery(req)
@ -149,13 +149,15 @@ func TestCreateValidation(t *testing.T) {
func(req UserCreateRequest, res *User) *User { func(req UserCreateRequest, res *User) *User {
return nil return nil
}, },
errors.New("Key: 'UserCreateRequest.name' Error:Field validation for 'name' failed on the 'required' tag\n" + errors.New("Key: 'UserCreateRequest.first_name' Error:Field validation for 'first_name' failed on the 'required' tag\n" +
"Key: 'UserCreateRequest.last_name' Error:Field validation for 'last_name' failed on the 'required' tag\n" +
"Key: 'UserCreateRequest.email' Error:Field validation for 'email' failed on the 'required' tag\n" + "Key: 'UserCreateRequest.email' Error:Field validation for 'email' failed on the 'required' tag\n" +
"Key: 'UserCreateRequest.password' Error:Field validation for 'password' failed on the 'required' tag"), "Key: 'UserCreateRequest.password' Error:Field validation for 'password' failed on the 'required' tag"),
}, },
{"Valid Email", {"Valid Email",
UserCreateRequest{ UserCreateRequest{
Name: "Lee Brown", FirstName: "Lee",
LastName: "Brown",
Email: "xxxxxxxxxx", Email: "xxxxxxxxxx",
Password: "akTechFr0n!ier", Password: "akTechFr0n!ier",
PasswordConfirm: "akTechFr0n!ier", PasswordConfirm: "akTechFr0n!ier",
@ -167,7 +169,8 @@ func TestCreateValidation(t *testing.T) {
}, },
{"Passwords Match", {"Passwords Match",
UserCreateRequest{ UserCreateRequest{
Name: "Lee Brown", FirstName: "Lee",
LastName: "Brown",
Email: uuid.NewRandom().String() + "@geeksinthewoods.com", Email: uuid.NewRandom().String() + "@geeksinthewoods.com",
Password: "akTechFr0n!ier", Password: "akTechFr0n!ier",
PasswordConfirm: "W0rkL1fe#", PasswordConfirm: "W0rkL1fe#",
@ -179,16 +182,18 @@ func TestCreateValidation(t *testing.T) {
}, },
{"Default Timezone", {"Default Timezone",
UserCreateRequest{ UserCreateRequest{
Name: "Lee Brown", FirstName: "Lee",
LastName: "Brown",
Email: uuid.NewRandom().String() + "@geeksinthewoods.com", Email: uuid.NewRandom().String() + "@geeksinthewoods.com",
Password: "akTechFr0n!ier", Password: "akTechFr0n!ier",
PasswordConfirm: "akTechFr0n!ier", PasswordConfirm: "akTechFr0n!ier",
}, },
func(req UserCreateRequest, res *User) *User { func(req UserCreateRequest, res *User) *User {
return &User{ return &User{
Name: req.Name, FirstName: "Lee",
Email: req.Email, LastName: "Brown",
Timezone: "America/Anchorage", Email: req.Email,
Timezone: "America/Anchorage",
// Copy this fields from the result. // Copy this fields from the result.
ID: res.ID, ID: res.ID,
@ -259,7 +264,8 @@ func TestCreateValidationEmailUnique(t *testing.T) {
ctx := tests.Context() ctx := tests.Context()
req1 := UserCreateRequest{ req1 := UserCreateRequest{
Name: "Lee Brown", FirstName: "Lee",
LastName: "Brown",
Email: uuid.NewRandom().String() + "@geeksinthewoods.com", Email: uuid.NewRandom().String() + "@geeksinthewoods.com",
Password: "akTechFr0n!ier", Password: "akTechFr0n!ier",
PasswordConfirm: "akTechFr0n!ier", PasswordConfirm: "akTechFr0n!ier",
@ -271,7 +277,8 @@ func TestCreateValidationEmailUnique(t *testing.T) {
} }
req2 := UserCreateRequest{ req2 := UserCreateRequest{
Name: "Lucas Brown", FirstName: "Lee",
LastName: "Brown",
Email: user1.Email, Email: user1.Email,
Password: "W0rkL1fe#", Password: "W0rkL1fe#",
PasswordConfirm: "W0rkL1fe#", PasswordConfirm: "W0rkL1fe#",
@ -307,7 +314,8 @@ func TestCreateClaims(t *testing.T) {
{"EmptyClaims", {"EmptyClaims",
auth.Claims{}, auth.Claims{},
UserCreateRequest{ UserCreateRequest{
Name: "Lee Brown", FirstName: "Lee",
LastName: "Brown",
Email: uuid.NewRandom().String() + "@geeksinthewoods.com", Email: uuid.NewRandom().String() + "@geeksinthewoods.com",
Password: "akTechFr0n!ier", Password: "akTechFr0n!ier",
PasswordConfirm: "akTechFr0n!ier", PasswordConfirm: "akTechFr0n!ier",
@ -324,7 +332,8 @@ func TestCreateClaims(t *testing.T) {
}, },
}, },
UserCreateRequest{ UserCreateRequest{
Name: "Lee Brown", FirstName: "Lee",
LastName: "Brown",
Email: uuid.NewRandom().String() + "@geeksinthewoods.com", Email: uuid.NewRandom().String() + "@geeksinthewoods.com",
Password: "akTechFr0n!ier", Password: "akTechFr0n!ier",
PasswordConfirm: "akTechFr0n!ier", PasswordConfirm: "akTechFr0n!ier",
@ -341,7 +350,8 @@ func TestCreateClaims(t *testing.T) {
}, },
}, },
UserCreateRequest{ UserCreateRequest{
Name: "Lee Brown", FirstName: "Lee",
LastName: "Brown",
Email: uuid.NewRandom().String() + "@geeksinthewoods.com", Email: uuid.NewRandom().String() + "@geeksinthewoods.com",
Password: "akTechFr0n!ier", Password: "akTechFr0n!ier",
PasswordConfirm: "akTechFr0n!ier", PasswordConfirm: "akTechFr0n!ier",
@ -441,7 +451,8 @@ func TestUpdateValidationEmailUnique(t *testing.T) {
ctx := tests.Context() ctx := tests.Context()
req1 := UserCreateRequest{ req1 := UserCreateRequest{
Name: "Lee Brown", FirstName: "Lee",
LastName: "Brown",
Email: uuid.NewRandom().String() + "@geeksinthewoods.com", Email: uuid.NewRandom().String() + "@geeksinthewoods.com",
Password: "akTechFr0n!ier", Password: "akTechFr0n!ier",
PasswordConfirm: "akTechFr0n!ier", PasswordConfirm: "akTechFr0n!ier",
@ -453,7 +464,8 @@ func TestUpdateValidationEmailUnique(t *testing.T) {
} }
req2 := UserCreateRequest{ req2 := UserCreateRequest{
Name: "Lucas Brown", FirstName: "Lee",
LastName: "Brown",
Email: uuid.NewRandom().String() + "@geeksinthewoods.com", Email: uuid.NewRandom().String() + "@geeksinthewoods.com",
Password: "W0rkL1fe#", Password: "W0rkL1fe#",
PasswordConfirm: "W0rkL1fe#", PasswordConfirm: "W0rkL1fe#",
@ -500,7 +512,8 @@ func TestUpdatePassword(t *testing.T) {
// Create a new user for testing. // Create a new user for testing.
initPass := uuid.NewRandom().String() initPass := uuid.NewRandom().String()
user, err := Create(ctx, auth.Claims{}, test.MasterDB, UserCreateRequest{ user, err := Create(ctx, auth.Claims{}, test.MasterDB, UserCreateRequest{
Name: "Lee Brown", FirstName: "Lee",
LastName: "Brown",
Email: uuid.NewRandom().String() + "@geeksinthewoods.com", Email: uuid.NewRandom().String() + "@geeksinthewoods.com",
Password: initPass, Password: initPass,
PasswordConfirm: initPass, PasswordConfirm: initPass,
@ -591,7 +604,8 @@ func TestCrud(t *testing.T) {
return auth.Claims{} return auth.Claims{}
}, },
UserCreateRequest{ UserCreateRequest{
Name: "Lee Brown", FirstName: "Lee",
LastName: "Brown",
Email: uuid.NewRandom().String() + "@geeksinthewoods.com", Email: uuid.NewRandom().String() + "@geeksinthewoods.com",
Password: "akTechFr0n!ier", Password: "akTechFr0n!ier",
PasswordConfirm: "akTechFr0n!ier", PasswordConfirm: "akTechFr0n!ier",
@ -609,7 +623,8 @@ func TestCrud(t *testing.T) {
Email: *req.Email, Email: *req.Email,
// Copy this fields from the created user. // Copy this fields from the created user.
ID: user.ID, ID: user.ID,
Name: user.Name, FirstName: user.FirstName,
LastName: user.LastName,
PasswordSalt: user.PasswordSalt, PasswordSalt: user.PasswordSalt,
PasswordHash: user.PasswordHash, PasswordHash: user.PasswordHash,
PasswordReset: user.PasswordReset, PasswordReset: user.PasswordReset,
@ -634,7 +649,8 @@ func TestCrud(t *testing.T) {
} }
}, },
UserCreateRequest{ UserCreateRequest{
Name: "Lee Brown", FirstName: "Lee",
LastName: "Brown",
Email: uuid.NewRandom().String() + "@geeksinthewoods.com", Email: uuid.NewRandom().String() + "@geeksinthewoods.com",
Password: "akTechFr0n!ier", Password: "akTechFr0n!ier",
PasswordConfirm: "akTechFr0n!ier", PasswordConfirm: "akTechFr0n!ier",
@ -665,7 +681,8 @@ func TestCrud(t *testing.T) {
} }
}, },
UserCreateRequest{ UserCreateRequest{
Name: "Lee Brown", FirstName: "Lee",
LastName: "Brown",
Email: uuid.NewRandom().String() + "@geeksinthewoods.com", Email: uuid.NewRandom().String() + "@geeksinthewoods.com",
Password: "akTechFr0n!ier", Password: "akTechFr0n!ier",
PasswordConfirm: "akTechFr0n!ier", PasswordConfirm: "akTechFr0n!ier",
@ -683,7 +700,8 @@ func TestCrud(t *testing.T) {
Email: *req.Email, Email: *req.Email,
// Copy this fields from the created user. // Copy this fields from the created user.
ID: user.ID, ID: user.ID,
Name: user.Name, FirstName: user.FirstName,
LastName: user.LastName,
PasswordSalt: user.PasswordSalt, PasswordSalt: user.PasswordSalt,
PasswordHash: user.PasswordHash, PasswordHash: user.PasswordHash,
PasswordReset: user.PasswordReset, PasswordReset: user.PasswordReset,
@ -708,7 +726,8 @@ func TestCrud(t *testing.T) {
} }
}, },
UserCreateRequest{ UserCreateRequest{
Name: "Lee Brown", FirstName: "Lee",
LastName: "Brown",
Email: uuid.NewRandom().String() + "@geeksinthewoods.com", Email: uuid.NewRandom().String() + "@geeksinthewoods.com",
Password: "akTechFr0n!ier", Password: "akTechFr0n!ier",
PasswordConfirm: "akTechFr0n!ier", PasswordConfirm: "akTechFr0n!ier",
@ -739,7 +758,8 @@ func TestCrud(t *testing.T) {
} }
}, },
UserCreateRequest{ UserCreateRequest{
Name: "Lee Brown", FirstName: "Lee",
LastName: "Brown",
Email: uuid.NewRandom().String() + "@geeksinthewoods.com", Email: uuid.NewRandom().String() + "@geeksinthewoods.com",
Password: "akTechFr0n!ier", Password: "akTechFr0n!ier",
PasswordConfirm: "akTechFr0n!ier", PasswordConfirm: "akTechFr0n!ier",
@ -757,7 +777,8 @@ func TestCrud(t *testing.T) {
Email: *req.Email, Email: *req.Email,
// Copy this fields from the created user. // Copy this fields from the created user.
ID: user.ID, ID: user.ID,
Name: user.Name, FirstName: user.FirstName,
LastName: user.LastName,
PasswordSalt: user.PasswordSalt, PasswordSalt: user.PasswordSalt,
PasswordHash: user.PasswordHash, PasswordHash: user.PasswordHash,
PasswordReset: user.PasswordReset, PasswordReset: user.PasswordReset,
@ -882,7 +903,8 @@ func TestFind(t *testing.T) {
var users []*User var users []*User
for i := 0; i <= 4; i++ { for i := 0; i <= 4; i++ {
user, err := Create(tests.Context(), auth.Claims{}, test.MasterDB, UserCreateRequest{ user, err := Create(tests.Context(), auth.Claims{}, test.MasterDB, UserCreateRequest{
Name: "Lee Brown", FirstName: "Lee",
LastName: "Brown",
Email: uuid.NewRandom().String() + "@geeksinthewoods.com", Email: uuid.NewRandom().String() + "@geeksinthewoods.com",
Password: "akTechFr0n!ier", Password: "akTechFr0n!ier",
PasswordConfirm: "akTechFr0n!ier", PasswordConfirm: "akTechFr0n!ier",

View File

@ -4,7 +4,7 @@ import (
"context" "context"
"database/sql" "database/sql"
"geeks-accelerator/oss/saas-starter-kit/internal/account" "geeks-accelerator/oss/saas-starter-kit/internal/account"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web" "geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext"
"time" "time"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/auth" "geeks-accelerator/oss/saas-starter-kit/internal/platform/auth"
@ -203,7 +203,7 @@ func Create(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req UserAc
defer span.Finish() defer span.Finish()
// Validate the request. // Validate the request.
v := web.NewValidator() v := webcontext.Validator()
err := v.Struct(req) err := v.Struct(req)
if err != nil { if err != nil {
return nil, err return nil, err
@ -318,7 +318,7 @@ func Update(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req UserAc
defer span.Finish() defer span.Finish()
// Validate the request. // Validate the request.
v := web.NewValidator() v := webcontext.Validator()
err := v.Struct(req) err := v.Struct(req)
if err != nil { if err != nil {
return err return err
@ -391,7 +391,7 @@ func Archive(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req UserA
defer span.Finish() defer span.Finish()
// Validate the request. // Validate the request.
v := web.NewValidator() v := webcontext.Validator()
err := v.Struct(req) err := v.Struct(req)
if err != nil { if err != nil {
return err return err
@ -443,7 +443,7 @@ func Delete(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req UserAc
defer span.Finish() defer span.Finish()
// Validate the request. // Validate the request.
v := web.NewValidator() v := webcontext.Validator()
err := v.Struct(req) err := v.Struct(req)
if err != nil { if err != nil {
return err return err