You've already forked golang-saas-starter-kit
mirror of
https://github.com/raseels-repos/golang-saas-starter-kit.git
synced 2025-08-08 22:36:41 +02:00
issue#16 web-app account signup
Account signup works with validation and translations.
This commit is contained in:
@ -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{
|
||||||
|
@ -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" {
|
||||||
|
@ -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 .
|
|
||||||
```
|
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
21
cmd/web-app/handlers/projects.go
Normal file
21
cmd/web-app/handlers/projects.go
Normal 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)
|
||||||
|
}
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
88
cmd/web-app/handlers/signup.go
Normal file
88
cmd/web-app/handlers/signup.go
Normal 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)
|
||||||
|
}
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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 |
18
cmd/web-app/templates/content/error-generic.gohtml
Normal file
18
cmd/web-app/templates/content/error-generic.gohtml
Normal 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 }}
|
116
cmd/web-app/templates/content/signup-step1.tmpl
Normal file
116
cmd/web-app/templates/content/signup-step1.tmpl
Normal 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}}
|
@ -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}}
|
@ -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 }}
|
66
cmd/web-app/templates/partials/page-wrapper.tmpl
Normal file
66
cmd/web-app/templates/partials/page-wrapper.tmpl
Normal 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 © 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 }}
|
68
cmd/web-app/templates/partials/sidebar.tmpl
Normal file
68
cmd/web-app/templates/partials/sidebar.tmpl
Normal 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 }}
|
184
cmd/web-app/templates/partials/topbar.tmpl
Normal file
184
cmd/web-app/templates/partials/topbar.tmpl
Normal 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
27
go.mod
@ -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
50
go.sum
@ -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=
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
45
internal/mid/custom_context.go
Normal file
45
internal/mid/custom_context.go
Normal 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
|
||||||
|
}
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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(),
|
||||||
|
@ -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)
|
||||||
|
62
internal/mid/translator.go
Normal file
62
internal/mid/translator.go
Normal 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
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
@ -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
|
|
||||||
}
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
@ -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 {
|
||||||
|
@ -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.
|
||||||
|
53
internal/platform/web/webcontext/context.go
Normal file
53
internal/platform/web/webcontext/context.go
Normal 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 ""
|
||||||
|
}
|
157
internal/platform/web/webcontext/transvalidate.go
Normal file
157
internal/platform/web/webcontext/transvalidate.go
Normal 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
|
||||||
|
}
|
121
internal/platform/web/weberror/error.go
Normal file
121
internal/platform/web/weberror/error.go
Normal 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
|
||||||
|
}
|
28
internal/platform/web/weberror/shutdown.go
Normal file
28
internal/platform/web/weberror/shutdown.go
Normal 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
|
||||||
|
}
|
103
internal/platform/web/weberror/validation.go
Normal file
103
internal/platform/web/weberror/validation.go
Normal 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, ".")
|
||||||
|
}
|
@ -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
|
||||||
|
@ -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
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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.
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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",
|
||||||
|
@ -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
|
||||||
|
Reference in New Issue
Block a user