You've already forked golang-saas-starter-kit
mirror of
https://github.com/raseels-repos/golang-saas-starter-kit.git
synced 2025-06-27 00:51:13 +02:00
fixed signup
This commit is contained in:
@ -47,6 +47,22 @@ func APP(shutdown chan os.Signal, log *log.Logger, env webcontext.Env, staticDir
|
|||||||
}
|
}
|
||||||
app.Handle("GET", "/projects", p.Index, mid.AuthenticateSessionRequired(authenticator), mid.HasAuth())
|
app.Handle("GET", "/projects", p.Index, mid.AuthenticateSessionRequired(authenticator), mid.HasAuth())
|
||||||
|
|
||||||
|
|
||||||
|
// Register user management pages.
|
||||||
|
us := Users{
|
||||||
|
MasterDB: masterDB,
|
||||||
|
Redis: redis,
|
||||||
|
Renderer: renderer,
|
||||||
|
Authenticator: authenticator,
|
||||||
|
ProjectRoutes: projectRoutes,
|
||||||
|
NotifyEmail: notifyEmail,
|
||||||
|
SecretKey: secretKey,
|
||||||
|
}
|
||||||
|
app.Handle("GET", "/users", us.Index, mid.AuthenticateSessionRequired(authenticator), mid.HasRole(auth.RoleAdmin))
|
||||||
|
app.Handle("POST", "/users/:user_id/update", us.Update, mid.AuthenticateSessionRequired(authenticator), mid.HasRole(auth.RoleAdmin))
|
||||||
|
app.Handle("GET", "/users/:user_id/update", us.Update, mid.AuthenticateSessionRequired(authenticator), mid.HasRole(auth.RoleAdmin))
|
||||||
|
app.Handle("GET", "/users/:user_id", us.View, mid.AuthenticateSessionRequired(authenticator), mid.HasRole(auth.RoleAdmin))
|
||||||
|
|
||||||
// Register user management and authentication endpoints.
|
// Register user management and authentication endpoints.
|
||||||
u := User{
|
u := User{
|
||||||
MasterDB: masterDB,
|
MasterDB: masterDB,
|
||||||
|
@ -36,19 +36,19 @@ func (h *Signup) Step1(ctx context.Context, w http.ResponseWriter, r *http.Reque
|
|||||||
//
|
//
|
||||||
req := new(signup.SignupRequest)
|
req := new(signup.SignupRequest)
|
||||||
data := make(map[string]interface{})
|
data := make(map[string]interface{})
|
||||||
f := func() error {
|
f := func() (bool, error) {
|
||||||
claims, _ := auth.ClaimsFromContext(ctx)
|
claims, _ := auth.ClaimsFromContext(ctx)
|
||||||
|
|
||||||
if r.Method == http.MethodPost {
|
if r.Method == http.MethodPost {
|
||||||
|
|
||||||
err := r.ParseForm()
|
err := r.ParseForm()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
decoder := schema.NewDecoder()
|
decoder := schema.NewDecoder()
|
||||||
if err := decoder.Decode(req, r.PostForm); err != nil {
|
if err := decoder.Decode(req, r.PostForm); err != nil {
|
||||||
return err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute the account / user signup.
|
// Execute the account / user signup.
|
||||||
@ -56,13 +56,13 @@ func (h *Signup) Step1(ctx context.Context, w http.ResponseWriter, r *http.Reque
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
switch errors.Cause(err) {
|
switch errors.Cause(err) {
|
||||||
case account.ErrForbidden:
|
case account.ErrForbidden:
|
||||||
return web.RespondError(ctx, w, weberror.NewError(ctx, err, http.StatusForbidden))
|
return false, web.RespondError(ctx, w, weberror.NewError(ctx, err, http.StatusForbidden))
|
||||||
default:
|
default:
|
||||||
if verr, ok := weberror.NewValidationError(ctx, err); ok {
|
if verr, ok := weberror.NewValidationError(ctx, err); ok {
|
||||||
data["validationErrors"] = verr.(*weberror.Error)
|
data["validationErrors"] = verr.(*weberror.Error)
|
||||||
return nil
|
return false, nil
|
||||||
} else {
|
} else {
|
||||||
return err
|
return false, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -70,13 +70,13 @@ func (h *Signup) Step1(ctx context.Context, w http.ResponseWriter, r *http.Reque
|
|||||||
// Authenticated the new user.
|
// Authenticated the new user.
|
||||||
token, err := user_auth.Authenticate(ctx, h.MasterDB, h.Authenticator, req.User.Email, req.User.Password, time.Hour, ctxValues.Now)
|
token, err := user_auth.Authenticate(ctx, h.MasterDB, h.Authenticator, req.User.Email, req.User.Password, time.Hour, ctxValues.Now)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the token to the users session.
|
// Add the token to the users session.
|
||||||
err = handleSessionToken(ctx, h.MasterDB, w, r, token)
|
err = handleSessionToken(ctx, h.MasterDB, w, r, token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Display a welcome message to the user.
|
// Display a welcome message to the user.
|
||||||
@ -85,19 +85,22 @@ func (h *Signup) Step1(ctx context.Context, w http.ResponseWriter, r *http.Reque
|
|||||||
"You workflow will be a breeze starting today.")
|
"You workflow will be a breeze starting today.")
|
||||||
err = webcontext.ContextSession(ctx).Save(r, w)
|
err = webcontext.ContextSession(ctx).Save(r, w)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redirect the user to the dashboard.
|
// Redirect the user to the dashboard.
|
||||||
http.Redirect(w, r, "/", http.StatusFound)
|
http.Redirect(w, r, "/", http.StatusFound)
|
||||||
return nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := f(); err != nil {
|
end, err := f()
|
||||||
|
if err != nil {
|
||||||
return web.RenderError(ctx, w, r, err, h.Renderer, TmplLayoutBase, TmplContentErrorGeneric, web.MIMETextHTMLCharsetUTF8)
|
return web.RenderError(ctx, w, r, err, h.Renderer, TmplLayoutBase, TmplContentErrorGeneric, web.MIMETextHTMLCharsetUTF8)
|
||||||
|
} else if end {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
data["geonameCountries"] = geonames.ValidGeonameCountries
|
data["geonameCountries"] = geonames.ValidGeonameCountries
|
||||||
|
@ -36,6 +36,7 @@ type User struct {
|
|||||||
SecretKey string
|
SecretKey string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UserLoginRequest extends the AuthenicateRequest with the RememberMe flag.
|
||||||
type UserLoginRequest struct {
|
type UserLoginRequest struct {
|
||||||
user_auth.AuthenticateRequest
|
user_auth.AuthenticateRequest
|
||||||
RememberMe bool
|
RememberMe bool
|
||||||
@ -120,33 +121,6 @@ func (h *User) Login(ctx context.Context, w http.ResponseWriter, r *http.Request
|
|||||||
return h.Renderer.Render(ctx, w, r, TmplLayoutBase, "user-login.gohtml", web.MIMETextHTMLCharsetUTF8, http.StatusOK, data)
|
return h.Renderer.Render(ctx, w, r, TmplLayoutBase, "user-login.gohtml", web.MIMETextHTMLCharsetUTF8, http.StatusOK, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleSessionToken persists the access token to the session for request authentication.
|
|
||||||
func handleSessionToken(ctx context.Context, db *sqlx.DB, w http.ResponseWriter, r *http.Request, token user_auth.Token) error {
|
|
||||||
if token.AccessToken == "" {
|
|
||||||
return errors.New("accessToken is required.")
|
|
||||||
}
|
|
||||||
|
|
||||||
sess := webcontext.ContextSession(ctx)
|
|
||||||
|
|
||||||
if sess.IsNew {
|
|
||||||
sess.ID = uuid.NewRandom().String()
|
|
||||||
}
|
|
||||||
|
|
||||||
sess.Options = &sessions.Options{
|
|
||||||
Path: "/",
|
|
||||||
MaxAge: int(token.TTL.Seconds()),
|
|
||||||
HttpOnly: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
sess = webcontext.SessionInit(sess,
|
|
||||||
token.AccessToken)
|
|
||||||
if err := sess.Save(r, w); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Logout handles removing authentication for the user.
|
// Logout handles removing authentication for the user.
|
||||||
func (h *User) Logout(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
func (h *User) Logout(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||||
|
|
||||||
@ -828,6 +802,33 @@ func (h *User) SwitchAccount(ctx context.Context, w http.ResponseWriter, r *http
|
|||||||
return h.Renderer.Render(ctx, w, r, TmplLayoutBase, "user-switch-account.gohtml", web.MIMETextHTMLCharsetUTF8, http.StatusOK, data)
|
return h.Renderer.Render(ctx, w, r, TmplLayoutBase, "user-switch-account.gohtml", web.MIMETextHTMLCharsetUTF8, http.StatusOK, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleSessionToken persists the access token to the session for request authentication.
|
||||||
|
func handleSessionToken(ctx context.Context, db *sqlx.DB, w http.ResponseWriter, r *http.Request, token user_auth.Token) error {
|
||||||
|
if token.AccessToken == "" {
|
||||||
|
return errors.New("accessToken is required.")
|
||||||
|
}
|
||||||
|
|
||||||
|
sess := webcontext.ContextSession(ctx)
|
||||||
|
|
||||||
|
if sess.IsNew {
|
||||||
|
sess.ID = uuid.NewRandom().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
sess.Options = &sessions.Options{
|
||||||
|
Path: "/",
|
||||||
|
MaxAge: int(token.TTL.Seconds()),
|
||||||
|
HttpOnly: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
sess = webcontext.SessionInit(sess,
|
||||||
|
token.AccessToken)
|
||||||
|
if err := sess.Save(r, w); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// updateContextClaims updates the claims in the context.
|
// updateContextClaims updates the claims in the context.
|
||||||
func updateContextClaims(ctx context.Context, authenticator *auth.Authenticator, claims auth.Claims) (context.Context, error) {
|
func updateContextClaims(ctx context.Context, authenticator *auth.Authenticator, claims auth.Claims) (context.Context, error) {
|
||||||
tkn, err := authenticator.GenerateToken(claims)
|
tkn, err := authenticator.GenerateToken(claims)
|
||||||
|
301
cmd/web-app/handlers/users.go
Normal file
301
cmd/web-app/handlers/users.go
Normal file
@ -0,0 +1,301 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"geeks-accelerator/oss/saas-starter-kit/internal/platform/datatable"
|
||||||
|
"geeks-accelerator/oss/saas-starter-kit/internal/geonames"
|
||||||
|
"geeks-accelerator/oss/saas-starter-kit/internal/platform/auth"
|
||||||
|
"geeks-accelerator/oss/saas-starter-kit/internal/platform/notify"
|
||||||
|
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web"
|
||||||
|
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext"
|
||||||
|
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/weberror"
|
||||||
|
project_routes "geeks-accelerator/oss/saas-starter-kit/internal/project-routes"
|
||||||
|
"geeks-accelerator/oss/saas-starter-kit/internal/user"
|
||||||
|
"geeks-accelerator/oss/saas-starter-kit/internal/user_account"
|
||||||
|
"github.com/gorilla/schema"
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gopkg.in/DataDog/dd-trace-go.v1/contrib/go-redis/redis"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Users represents the Users API method handler set.
|
||||||
|
type Users struct {
|
||||||
|
MasterDB *sqlx.DB
|
||||||
|
Redis *redis.Client
|
||||||
|
Renderer web.Renderer
|
||||||
|
Authenticator *auth.Authenticator
|
||||||
|
ProjectRoutes project_routes.ProjectRoutes
|
||||||
|
NotifyEmail notify.Email
|
||||||
|
SecretKey string
|
||||||
|
}
|
||||||
|
|
||||||
|
func UrlUsersView(userID string) string {
|
||||||
|
return fmt.Sprintf("/users/%s", userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Index handles listing all the users for the current account.
|
||||||
|
func (h *Users) Index(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||||
|
|
||||||
|
claims, err := auth.ClaimsFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var statusValues []interface{}
|
||||||
|
for _, v := range user_account.UserAccountStatus_Values {
|
||||||
|
statusValues = append(statusValues, string(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
statusOpts := web.NewEnumResponse(ctx, nil, statusValues...)
|
||||||
|
|
||||||
|
statusFilterItems := []datatable.FilterOptionItem{}
|
||||||
|
for _, opt := range statusOpts.Options {
|
||||||
|
statusFilterItems = append(statusFilterItems, datatable.FilterOptionItem{
|
||||||
|
Display: opt.Title,
|
||||||
|
Value: opt.Value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fields := []datatable.DisplayField{
|
||||||
|
datatable.DisplayField{Field: "id", Title: "ID", Visible: false, Searchable: true, Orderable: true, Filterable: false},
|
||||||
|
datatable.DisplayField{Field: "name", Title: "User", Visible: true, Searchable: true, Orderable: true, Filterable: true, FilterPlaceholder: "filter Name"},
|
||||||
|
datatable.DisplayField{Field: "status", Title: "Status", Visible: true, Searchable: true, Orderable: true, Filterable: true, FilterPlaceholder: "All Statuses", FilterItems: statusFilterItems},
|
||||||
|
datatable.DisplayField{Field: "updated_at", Title: "Last Updated", Visible: true, Searchable: true, Orderable: true, Filterable: false},
|
||||||
|
datatable.DisplayField{Field: "created_at", Title: "Created", Visible: true, Searchable: true, Orderable: true, Filterable: false},
|
||||||
|
}
|
||||||
|
|
||||||
|
mapFunc := func(q *user_account.User, cols []datatable.DisplayField) (resp []datatable.ColumnValue, err error) {
|
||||||
|
for i := 0; i < len(cols); i++ {
|
||||||
|
col := cols[i]
|
||||||
|
var v datatable.ColumnValue
|
||||||
|
switch col.Field {
|
||||||
|
case "id":
|
||||||
|
v.Value = fmt.Sprintf("%d", q.ID)
|
||||||
|
case "name":
|
||||||
|
v.Value = q.Name
|
||||||
|
v.Formatted = fmt.Sprintf("<a href='%s'>%s</a>", UrlUsersView(q.ID), v.Value)
|
||||||
|
case "created_at":
|
||||||
|
dt := web.NewTimeResponse(ctx, q.CreatedAt)
|
||||||
|
v.Value = dt.Local
|
||||||
|
v.Formatted = fmt.Sprintf("<span class='cell-font-date'>%s</span>", v.Value)
|
||||||
|
case "updated_at":
|
||||||
|
dt := web.NewTimeResponse(ctx, q.UpdatedAt)
|
||||||
|
v.Value = dt.Local
|
||||||
|
v.Formatted = fmt.Sprintf("<span class='cell-font-date'>%s</span>", v.Value)
|
||||||
|
default:
|
||||||
|
return resp, errors.Errorf("Failed to map value for %s.", col.Field)
|
||||||
|
}
|
||||||
|
resp = append(resp, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
loadFunc := func(ctx context.Context, sorting string, fields []datatable.DisplayField) (resp [][]datatable.ColumnValue, err error) {
|
||||||
|
res, err := user_account.UserFindByAccount(ctx, claims, h.MasterDB, user_account.UserFindByAccountRequest{
|
||||||
|
Order: strings.Split(sorting, ","),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, a := range res {
|
||||||
|
l, err := mapFunc(a, fields)
|
||||||
|
if err != nil {
|
||||||
|
return resp, errors.Wrapf(err, "Failed to map user for display.")
|
||||||
|
}
|
||||||
|
|
||||||
|
resp = append(resp, l)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
dt, err := datatable.New(ctx, w, r, h.Redis, fields, loadFunc)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if dt.HasCache() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok, err := dt.Render(); ok {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"datatable": dt.Response(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return h.Renderer.Render(ctx, w, r, TmplLayoutBase, "users-index.gohtml", web.MIMETextHTMLCharsetUTF8, http.StatusOK, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// View handles displaying a user.
|
||||||
|
func (h *Users) View(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||||
|
|
||||||
|
data := make(map[string]interface{})
|
||||||
|
f := func() error {
|
||||||
|
|
||||||
|
claims, err := auth.ClaimsFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
usr, err := user.ReadByID(ctx, claims, h.MasterDB, claims.Subject)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
data["user"] = usr.Response(ctx)
|
||||||
|
|
||||||
|
usrAccs, err := user_account.FindByUserID(ctx, claims, h.MasterDB, claims.Subject, false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, usrAcc := range usrAccs {
|
||||||
|
if usrAcc.AccountID == claims.Audience {
|
||||||
|
data["userAccount"] = usrAcc.Response(ctx)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := f(); err != nil {
|
||||||
|
return web.RenderError(ctx, w, r, err, h.Renderer, TmplLayoutBase, TmplContentErrorGeneric, web.MIMETextHTMLCharsetUTF8)
|
||||||
|
}
|
||||||
|
|
||||||
|
return h.Renderer.Render(ctx, w, r, TmplLayoutBase, "users-view.gohtml", web.MIMETextHTMLCharsetUTF8, http.StatusOK, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update handles updating a user for the account.
|
||||||
|
func (h *Users) Update(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||||
|
|
||||||
|
ctxValues, err := webcontext.ContextValues(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, err := auth.ClaimsFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
req := new(user.UserUpdateRequest)
|
||||||
|
data := make(map[string]interface{})
|
||||||
|
f := func() (bool, error) {
|
||||||
|
if r.Method == http.MethodPost {
|
||||||
|
err := r.ParseForm()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
decoder := schema.NewDecoder()
|
||||||
|
decoder.IgnoreUnknownKeys(true)
|
||||||
|
|
||||||
|
if err := decoder.Decode(req, r.PostForm); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
req.ID = claims.Subject
|
||||||
|
|
||||||
|
err = user.Update(ctx, claims, h.MasterDB, *req, ctxValues.Now)
|
||||||
|
if err != nil {
|
||||||
|
switch errors.Cause(err) {
|
||||||
|
default:
|
||||||
|
if verr, ok := weberror.NewValidationError(ctx, err); ok {
|
||||||
|
data["validationErrors"] = verr.(*weberror.Error)
|
||||||
|
return false, nil
|
||||||
|
} else {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.PostForm.Get("Password") != "" {
|
||||||
|
pwdReq := new(user.UserUpdatePasswordRequest)
|
||||||
|
|
||||||
|
if err := decoder.Decode(pwdReq, r.PostForm); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
pwdReq.ID = claims.Subject
|
||||||
|
|
||||||
|
err = user.UpdatePassword(ctx, claims, h.MasterDB, *pwdReq, ctxValues.Now)
|
||||||
|
if err != nil {
|
||||||
|
switch errors.Cause(err) {
|
||||||
|
default:
|
||||||
|
if verr, ok := weberror.NewValidationError(ctx, err); ok {
|
||||||
|
data["validationErrors"] = verr.(*weberror.Error)
|
||||||
|
return false, nil
|
||||||
|
} else {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display a success message to the user.
|
||||||
|
webcontext.SessionFlashSuccess(ctx,
|
||||||
|
"User Updated",
|
||||||
|
"User successfully updated.")
|
||||||
|
err = webcontext.ContextSession(ctx).Save(r, w)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Redirect(w, r, "/users/"+req.ID, http.StatusFound)
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
end, err := f()
|
||||||
|
if err != nil {
|
||||||
|
return web.RenderError(ctx, w, r, err, h.Renderer, TmplLayoutBase, TmplContentErrorGeneric, web.MIMETextHTMLCharsetUTF8)
|
||||||
|
} else if end {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
usr, err := user.ReadByID(ctx, claims, h.MasterDB, claims.Subject)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.ID == "" {
|
||||||
|
req.FirstName = &usr.FirstName
|
||||||
|
req.LastName = &usr.LastName
|
||||||
|
req.Email = &usr.Email
|
||||||
|
req.Timezone = &usr.Timezone
|
||||||
|
}
|
||||||
|
|
||||||
|
data["user"] = usr.Response(ctx)
|
||||||
|
|
||||||
|
data["timezones"], err = geonames.ListTimezones(ctx, h.MasterDB)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
data["form"] = req
|
||||||
|
|
||||||
|
if verr, ok := weberror.NewValidationError(ctx, webcontext.Validator().Struct(user.UserUpdateRequest{})); ok {
|
||||||
|
data["userValidationDefaults"] = verr.(*weberror.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
if verr, ok := weberror.NewValidationError(ctx, webcontext.Validator().Struct(user.UserUpdatePasswordRequest{})); ok {
|
||||||
|
data["passwordValidationDefaults"] = verr.(*weberror.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return h.Renderer.Render(ctx, w, r, TmplLayoutBase, "users-update.gohtml", web.MIMETextHTMLCharsetUTF8, http.StatusOK, data)
|
||||||
|
}
|
||||||
|
|
27
cmd/web-app/templates/content/users-index.gohtml
Normal file
27
cmd/web-app/templates/content/users-index.gohtml
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{{define "title"}}Users{{end}}
|
||||||
|
{{define "content"}}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<form method="post">
|
||||||
|
<div class="card">
|
||||||
|
<div class="table-responsive dataTable_card">
|
||||||
|
{{ template "partials/datatable/html" . }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{define "style"}}
|
||||||
|
{{ template "partials/datatable/style" . }}
|
||||||
|
{{ end }}
|
||||||
|
{{define "js"}}
|
||||||
|
{{ template "partials/datatable/js" . }}
|
||||||
|
|
||||||
|
<script>
|
||||||
|
$(document).ready(function(){
|
||||||
|
//$("#dataTable_filter").hide();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{{end}}
|
136
cmd/web-app/templates/partials/datatable.tmpl
Normal file
136
cmd/web-app/templates/partials/datatable.tmpl
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
{{ define "partials/datatable/html" }}
|
||||||
|
<table id="dataTable" class="display nowrap table table-hover table-striped table-bordered" cellspacing="0" width="100%">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
{{ range $idx, $c := .datatable.DisplayFields }}
|
||||||
|
<th>{{ $c.Title }}</th>
|
||||||
|
{{ end }}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
{{ range $idx, $c := .datatable.DisplayFields }}
|
||||||
|
<th>{{ $c.Title }}</th>
|
||||||
|
{{ end }}
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
{{ end }}
|
||||||
|
{{ define "partials/datatable/style" }}
|
||||||
|
<link href="{{ SiteAssetUrl "/assets/vendor/datatables/dataTables.bootstrap4.min.css" }}" rel="stylesheet">
|
||||||
|
{{ end }}
|
||||||
|
{{ define "partials/datatable/js" }}
|
||||||
|
<!-- This is data table -->
|
||||||
|
<script src="{{ SiteAssetUrl "/assets/vendor/datatables/jquery.dataTables.min.js" }}"></script>
|
||||||
|
<script>
|
||||||
|
$(document).ready(function() {
|
||||||
|
var dtbl = $('#dataTable').DataTable( {
|
||||||
|
serverSide: true,
|
||||||
|
ordering: true,
|
||||||
|
searching: true,
|
||||||
|
ajax: "{{ .datatable.AjaxUrl }}",
|
||||||
|
scrollY: 300,
|
||||||
|
scroller: {
|
||||||
|
loadingIndicator: true
|
||||||
|
},
|
||||||
|
scrollX: true,
|
||||||
|
stateSave: false,
|
||||||
|
"columnDefs": [
|
||||||
|
{{ range $idx, $c := .datatable.DisplayFields }}
|
||||||
|
{ "title": "{{ $c.Title }}", "name": "{{ $c.Field }}", "visible": {{ $c.Visible }}, "searchable": {{ $c.Searchable }}, "orderable": {{ $c.Orderable }}, "targets": {{ $idx }} },
|
||||||
|
{{ end }}
|
||||||
|
],
|
||||||
|
initComplete: function () {
|
||||||
|
{{ range $idx, $c := .datatable.DisplayFields }}
|
||||||
|
{{ if or $c.Filterable $c.AutocompletePath }}
|
||||||
|
|
||||||
|
this.api().columns({{ $idx }}).every( function (colIdx) {
|
||||||
|
var column = this;
|
||||||
|
|
||||||
|
{{ if or ($c.AutocompletePath) ($c.FilterItems) }}
|
||||||
|
var select = $('<select><option value="">{{ $c.FilterPlaceholder }}</option></select>')
|
||||||
|
.appendTo( $(column.footer()).empty() )
|
||||||
|
.on( 'change', function () {
|
||||||
|
var val = $.fn.dataTable.util.escapeRegex(
|
||||||
|
$(this).val()
|
||||||
|
);
|
||||||
|
|
||||||
|
column
|
||||||
|
.search(val ? '^' + val + '$' : '', true, false)
|
||||||
|
.draw();
|
||||||
|
} );
|
||||||
|
{{ if $c.AutocompletePath }}
|
||||||
|
$.ajax({
|
||||||
|
type: "GET",
|
||||||
|
url: '{{ $c.AutocompletePath }}',
|
||||||
|
dataType: "json",
|
||||||
|
success: function (data) {
|
||||||
|
for (var k in data.suggestions) {
|
||||||
|
kv = data.suggestions[k]
|
||||||
|
select.append( '<option value="'+kv.value+'">'+kv.data+'</option>' )
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
{{ else }}
|
||||||
|
{{ range $idx, $item := $c.FilterItems }}
|
||||||
|
select.append( '<option value="{{ $item.Value }}">{{ $item.Display }}</option>' )
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
{{ else }}
|
||||||
|
var input = $('<input type="text" placeholder="{{ $c.FilterPlaceholder }}" />')
|
||||||
|
.appendTo( $(column.footer()).empty() )
|
||||||
|
.on( 'change', function () {
|
||||||
|
if ( column.search() !== this.value ) {
|
||||||
|
column
|
||||||
|
.search( this.value )
|
||||||
|
.draw();
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
{{ end }}
|
||||||
|
} );
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
|
||||||
|
dtbl.on( 'draw', function () {
|
||||||
|
if ( typeof customPageDatatableDraw === "function" ) {
|
||||||
|
customPageDatatableDraw();
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
|
||||||
|
var vars = [], hash,filter_column,filter_value,filer_column_num;
|
||||||
|
var hashes = window.location.href.slice(window.location.href.indexOf('?') + 1).split('&');
|
||||||
|
|
||||||
|
if (hashes.length > 0 ) {
|
||||||
|
for(var i = 0; i < hashes.length; i++)
|
||||||
|
{
|
||||||
|
hash = hashes[i].split('=');
|
||||||
|
if (hash[0] == "filter_column") {
|
||||||
|
filter_column = hash[1].toLowerCase();
|
||||||
|
}
|
||||||
|
if (hash[0] == "filter_value") {
|
||||||
|
filter_value = hash[1].toLowerCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (filter_column && filter_value ) {
|
||||||
|
$( "#dataTable_wrapper thead th" ).each(function( index ) {
|
||||||
|
//console.log( index + ": " + $( this ).text() );
|
||||||
|
column_text = $( this ).text().toLowerCase();
|
||||||
|
column_text = column_text.replace(" ", "_");
|
||||||
|
if (column_text ==filter_column) {
|
||||||
|
filer_column_num = index;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (filer_column_num ) {
|
||||||
|
//console.log(filer_column_num);
|
||||||
|
dtbl.column(filer_column_num).search(filter_value).draw();
|
||||||
|
//filer_column_num = filer_column_num +1 ;
|
||||||
|
//console.log($(".dataTables_scrollFootInner tfoot th:nth-child("+filer_column_num+") ").text());
|
||||||
|
//$(".dataTables_scrollFootInner tfoot th:nth-child("+filer_column_num+") select ").val(filter_value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{{ end }}
|
592
internal/platform/datatable/datatable.go
Normal file
592
internal/platform/datatable/datatable.go
Normal file
@ -0,0 +1,592 @@
|
|||||||
|
package datatable
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web"
|
||||||
|
"github.com/pborman/uuid"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gopkg.in/DataDog/dd-trace-go.v1/contrib/go-redis/redis"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DatatableStateCacheTtl = 120
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrInvalidColumn occurs when a column can not be mapped correctly.
|
||||||
|
ErrInvalidColumn = errors.New("Invalid column")
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Datatable struct {
|
||||||
|
ctx context.Context
|
||||||
|
w http.ResponseWriter
|
||||||
|
r *http.Request
|
||||||
|
redis *redis.Client
|
||||||
|
fields []DisplayField
|
||||||
|
loadFunc func(ctx context.Context, sorting string, fields []DisplayField) (resp [][]ColumnValue, err error)
|
||||||
|
stateId string
|
||||||
|
req Request
|
||||||
|
resp *Response
|
||||||
|
handleRequest bool
|
||||||
|
sorting []string
|
||||||
|
cacheKey string
|
||||||
|
all [][]ColumnValue
|
||||||
|
loaded bool
|
||||||
|
storeFilteredFieldName string
|
||||||
|
filteredFieldValues []string
|
||||||
|
disableCache bool
|
||||||
|
caseSensitive bool
|
||||||
|
}
|
||||||
|
Request struct {
|
||||||
|
Data string
|
||||||
|
Columns map[int]Column
|
||||||
|
Order map[int]Order
|
||||||
|
Length int
|
||||||
|
Start int
|
||||||
|
Draw int
|
||||||
|
Search Search
|
||||||
|
}
|
||||||
|
Column struct {
|
||||||
|
Name string
|
||||||
|
Data string
|
||||||
|
Orderable bool
|
||||||
|
Searchable bool
|
||||||
|
Search Search
|
||||||
|
}
|
||||||
|
ColumnValue struct {
|
||||||
|
Value string
|
||||||
|
Formatted string
|
||||||
|
}
|
||||||
|
Search struct {
|
||||||
|
Value string
|
||||||
|
IsRegex bool
|
||||||
|
Regexp *regexp.Regexp
|
||||||
|
}
|
||||||
|
Order struct {
|
||||||
|
Column int
|
||||||
|
Dir string
|
||||||
|
}
|
||||||
|
Response struct {
|
||||||
|
AjaxUrl string `json:"ajaxUrl"`
|
||||||
|
Draw int `json:"draw"`
|
||||||
|
RecordsTotal int `json:"recordsTotal"`
|
||||||
|
RecordsFiltered int `json:"recordsFiltered"`
|
||||||
|
Data [][]string `json:"data"`
|
||||||
|
Error string `json:"error"`
|
||||||
|
DisplayFields []DisplayField `json:"displayFields"`
|
||||||
|
}
|
||||||
|
DisplayField struct {
|
||||||
|
Field string `json:"field"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Visible bool `json:"visible"`
|
||||||
|
Searchable bool `json:"searchable"`
|
||||||
|
Orderable bool `json:"orderable"`
|
||||||
|
Filterable bool `json:"filterable"`
|
||||||
|
AutocompletePath string `json:"autoComplete_path"`
|
||||||
|
FilterItems []FilterOptionItem `json:"filter_items"`
|
||||||
|
FilterPlaceholder string `json:"filter_placeholder"`
|
||||||
|
OrderFields []string `json:"order_fields"`
|
||||||
|
//Type string `json:"type"`
|
||||||
|
}
|
||||||
|
FilterOptionItem struct {
|
||||||
|
Value string `json:"value"`
|
||||||
|
Display string `json:"display"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (r Request) CacheKey() string {
|
||||||
|
c := Request{
|
||||||
|
Order: r.Order,
|
||||||
|
// Search : r.Search,
|
||||||
|
}
|
||||||
|
for _, cn := range r.Columns {
|
||||||
|
// these will be applied as a filter
|
||||||
|
cn.Search = Search{}
|
||||||
|
}
|
||||||
|
dat, _ := json.Marshal(c)
|
||||||
|
return fmt.Sprintf("%x", md5.Sum(dat))
|
||||||
|
}
|
||||||
|
|
||||||
|
// &columns[0][data]=&columns[0][name]=&columns[0][searchable]=true&columns[0][orderable]=false&columns[0][search][value]=&columns[0][search][regex]=false&columns[1][data]=ts&columns[1][name]=Time&columns[1][searchable]=true&columns[1][orderable]=true&columns[1][search][value]=&columns[1][search][regex]=false&columns[2][data]=level&columns[2][name]=Time&columns[2][searchable]=true&columns[2][orderable]=true&columns[2][search][value]=&columns[2][search][regex]=false&columns[3][data]=msg&columns[3][name]=Time&columns[3][searchable]=true&columns[3][orderable]=true&columns[3][search][value]=&columns[3][search][regex]=false&order[0][column]=0&order[0][dir]=asc&start=0&length=10&search[value]=&search[regex]=false&_=1537426209765
|
||||||
|
func ParseQueryValues(vals url.Values) (Request, error) {
|
||||||
|
|
||||||
|
req := Request{
|
||||||
|
Columns:make(map[int]Column),
|
||||||
|
Order: make(map[int]Order) ,
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
for kn, kvs := range vals {
|
||||||
|
|
||||||
|
pts := strings.Split(kn, "[")
|
||||||
|
switch pts[0] {
|
||||||
|
case "columns":
|
||||||
|
idxStr := strings.Split(pts[1], "]")[0]
|
||||||
|
|
||||||
|
idx, err := strconv.Atoi(idxStr)
|
||||||
|
if err != nil {
|
||||||
|
return req, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := req.Columns[idx]; !ok {
|
||||||
|
req.Columns[idx] = Column{}
|
||||||
|
}
|
||||||
|
curCol := req.Columns[idx]
|
||||||
|
|
||||||
|
sn := strings.Split(pts[2], "]")[0]
|
||||||
|
switch sn {
|
||||||
|
case "name":
|
||||||
|
curCol.Name = kvs[0]
|
||||||
|
case "data":
|
||||||
|
curCol.Data = kvs[0]
|
||||||
|
case "orderable":
|
||||||
|
if kvs[0] != "" {
|
||||||
|
curCol.Orderable, err = strconv.ParseBool(kvs[0])
|
||||||
|
if err != nil {
|
||||||
|
return req, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "searchable":
|
||||||
|
if kvs[0] != "" {
|
||||||
|
curCol.Searchable, err = strconv.ParseBool(kvs[0])
|
||||||
|
if err != nil {
|
||||||
|
return req, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "search":
|
||||||
|
svn := strings.Split(pts[3], "]")[0]
|
||||||
|
switch svn {
|
||||||
|
case "regex":
|
||||||
|
if kvs[0] != "" {
|
||||||
|
curCol.Search.IsRegex, err = strconv.ParseBool(kvs[0])
|
||||||
|
if err != nil {
|
||||||
|
return req, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "value":
|
||||||
|
if strings.ToLower(kvs[0]) != "false" {
|
||||||
|
curCol.Search.Value = kvs[0]
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return req, errors.WithMessagef(ErrInvalidColumn, "Unable to map query Column Search %s for %s", svn, kn)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return req, errors.WithMessagef(ErrInvalidColumn,"Unable to map query Column %s for %s", sn, kn)
|
||||||
|
}
|
||||||
|
req.Columns[idx] = curCol
|
||||||
|
case "order":
|
||||||
|
idxStr := strings.Split(pts[1], "]")[0]
|
||||||
|
|
||||||
|
idx, err := strconv.Atoi(idxStr)
|
||||||
|
if err != nil {
|
||||||
|
return req, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := req.Order[idx]; !ok {
|
||||||
|
req.Order[idx] = Order{}
|
||||||
|
}
|
||||||
|
curOrder := req.Order[idx]
|
||||||
|
|
||||||
|
sn := strings.Split(pts[2], "]")[0]
|
||||||
|
switch sn {
|
||||||
|
case "dir":
|
||||||
|
curOrder.Dir = kvs[0]
|
||||||
|
case "column":
|
||||||
|
if kvs[0] != "" {
|
||||||
|
curOrder.Column, err = strconv.Atoi(kvs[0])
|
||||||
|
if err != nil {
|
||||||
|
return req, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return req, errors.WithMessagef(ErrInvalidColumn, "Unable to map query Order %s for %s", sn, kn)
|
||||||
|
}
|
||||||
|
req.Order[idx] = curOrder
|
||||||
|
case "length":
|
||||||
|
if kvs[0] != "" {
|
||||||
|
req.Length, err = strconv.Atoi(kvs[0])
|
||||||
|
if err != nil {
|
||||||
|
return req, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "draw":
|
||||||
|
if kvs[0] != "" {
|
||||||
|
req.Draw, err = strconv.Atoi(kvs[0])
|
||||||
|
if err != nil {
|
||||||
|
return req, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "start":
|
||||||
|
if kvs[0] != "" {
|
||||||
|
req.Start, err = strconv.Atoi(kvs[0])
|
||||||
|
if err != nil {
|
||||||
|
return req, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
case "search":
|
||||||
|
sn := strings.Split(pts[1], "]")[0]
|
||||||
|
switch sn {
|
||||||
|
case "value":
|
||||||
|
if strings.ToLower(kvs[0]) != "false" {
|
||||||
|
req.Search.Value = kvs[0]
|
||||||
|
}
|
||||||
|
case "regex":
|
||||||
|
if kvs[0] != "" {
|
||||||
|
req.Search.IsRegex, err = strconv.ParseBool(kvs[0])
|
||||||
|
if err != nil {
|
||||||
|
return req, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return req, errors.WithMessagef(ErrInvalidColumn, "Unable to map query Order %s for %s", sn, kn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Search.IsRegex && req.Search.Value != "" {
|
||||||
|
req.Search.Regexp, err = regexp.Compile(req.Search.Value)
|
||||||
|
if err != nil {
|
||||||
|
return req, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for idx, col := range req.Columns {
|
||||||
|
if col.Search.IsRegex && col.Search.Value != "" {
|
||||||
|
col.Search.Regexp, err = regexp.Compile(col.Search.Value)
|
||||||
|
if err != nil {
|
||||||
|
return req, err
|
||||||
|
}
|
||||||
|
req.Columns[idx] = col
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(ctx context.Context, w http.ResponseWriter, r *http.Request, redisClient *redis.Client, fields []DisplayField, loadFunc func(ctx context.Context, sorting string, fields []DisplayField) (resp [][]ColumnValue, err error)) (dt *Datatable, err error) {
|
||||||
|
dt = &Datatable{
|
||||||
|
ctx: ctx,
|
||||||
|
w: w,
|
||||||
|
r: r,
|
||||||
|
redis: redisClient,
|
||||||
|
fields: fields,
|
||||||
|
loadFunc: loadFunc,
|
||||||
|
}
|
||||||
|
|
||||||
|
dt.stateId = r.URL.Query().Get("dtid")
|
||||||
|
if dt.stateId == "" {
|
||||||
|
dt.stateId = uuid.NewRandom().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
dt.resp = &Response{
|
||||||
|
Data: [][]string{},
|
||||||
|
}
|
||||||
|
dt.SetAjaxUrl(r.URL)
|
||||||
|
|
||||||
|
if web.RequestIsJson(r) {
|
||||||
|
dt.handleRequest = true
|
||||||
|
|
||||||
|
dt.req, err = ParseQueryValues(r.URL.Query())
|
||||||
|
if err != nil {
|
||||||
|
return dt, errors.Wrapf(err, "Failed to parse query values")
|
||||||
|
}
|
||||||
|
dt.resp.Draw = dt.req.Draw
|
||||||
|
|
||||||
|
dt.sorting = []string{}
|
||||||
|
for i := 0; i < len(dt.req.Order); i++ {
|
||||||
|
co := dt.req.Order[i]
|
||||||
|
|
||||||
|
cn := dt.req.Columns[co.Column]
|
||||||
|
|
||||||
|
var df DisplayField
|
||||||
|
for _, dc := range dt.fields {
|
||||||
|
if dc.Field == cn.Name {
|
||||||
|
df = dc
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if df.Field == "" {
|
||||||
|
err = errors.Errorf("Failed to find field for column %s", cn.Name)
|
||||||
|
return dt, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(df.OrderFields) > 0 {
|
||||||
|
for _, of := range df.OrderFields {
|
||||||
|
dt.sorting = append(dt.sorting, fmt.Sprintf("%s %s", of, co.Dir))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dt.sorting = append(dt.sorting, fmt.Sprintf("%s %s", df.Field, co.Dir))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < len(dt.req.Columns); i++ {
|
||||||
|
cn := dt.req.Columns[i]
|
||||||
|
|
||||||
|
var cf string
|
||||||
|
for _, dc := range dt.fields {
|
||||||
|
if dc.Field == cn.Name {
|
||||||
|
if dc.Filterable {
|
||||||
|
cn.Searchable = true
|
||||||
|
dt.req.Columns[i] = cn
|
||||||
|
}
|
||||||
|
|
||||||
|
cf = dc.Field
|
||||||
|
dt.resp.DisplayFields = append(dt.resp.DisplayFields, dc)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if cf == "" {
|
||||||
|
err = errors.Errorf("Failed to find field for column %s", cn.Name)
|
||||||
|
return dt, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dt.cacheKey = fmt.Sprintf("%x", md5.Sum([]byte(dt.resp.AjaxUrl + dt.req.CacheKey() + dt.stateId)))
|
||||||
|
|
||||||
|
} else {
|
||||||
|
//for idx, f := range fields {
|
||||||
|
// if f.Filterable && !f.Searchable {
|
||||||
|
// f.Searchable = true
|
||||||
|
// fields[idx] = f
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
|
dt.resp.DisplayFields = fields
|
||||||
|
}
|
||||||
|
|
||||||
|
return dt, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dt *Datatable) CaseSensitive() {
|
||||||
|
dt.caseSensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dt *Datatable) SetAjaxUrl(u *url.URL) {
|
||||||
|
un, _ := url.Parse(u.String())
|
||||||
|
|
||||||
|
qStr := un.Query()
|
||||||
|
qStr.Set("dtid", dt.stateId)
|
||||||
|
// add query to url
|
||||||
|
un.RawQuery = qStr.Encode()
|
||||||
|
|
||||||
|
if u.IsAbs() {
|
||||||
|
dt.resp.AjaxUrl = un.String()
|
||||||
|
} else {
|
||||||
|
dt.resp.AjaxUrl = un.RequestURI()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dt *Datatable) HasCache() bool {
|
||||||
|
if !dt.handleRequest || dt.disableCache {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// @TODO: Need to handle is error better. But when cache is down, the page should still respond,
|
||||||
|
// maybe just need to add logging.
|
||||||
|
cv, _ := dt.redis.WithContext(dt.ctx).Get(dt.cacheKey).Bytes()
|
||||||
|
|
||||||
|
if len(cv) > 0 {
|
||||||
|
err := json.Unmarshal(cv, &dt.all)
|
||||||
|
if err != nil {
|
||||||
|
// @TODO: Log the error here.
|
||||||
|
} else {
|
||||||
|
dt.loaded = true
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dt *Datatable) Handled() bool {
|
||||||
|
return dt.handleRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dt *Datatable) Response() Response {
|
||||||
|
return *dt.resp
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dt *Datatable) StoreFilteredField(cn string) {
|
||||||
|
dt.storeFilteredFieldName = cn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dt *Datatable) GetFilteredFieldValues() []string {
|
||||||
|
return dt.filteredFieldValues
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dt *Datatable) DisableCache() {
|
||||||
|
dt.disableCache = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dt *Datatable) Render() (rendered bool, err error) {
|
||||||
|
rendered = dt.handleRequest
|
||||||
|
if !rendered {
|
||||||
|
return rendered, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !dt.loaded {
|
||||||
|
sorting := strings.Join(dt.sorting, ",")
|
||||||
|
|
||||||
|
dt.all, err = dt.loadFunc(dt.ctx, sorting, dt.fields)
|
||||||
|
if err != nil {
|
||||||
|
return rendered, errors.Wrap(err, "Failed to load data")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !dt.disableCache {
|
||||||
|
dat, err := json.Marshal(dt.all)
|
||||||
|
if err != nil {
|
||||||
|
return rendered, errors.Wrap(err, "Failed to json encode cache response")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = dt.redis.WithContext(dt.ctx).Set(dt.cacheKey, dat, DatatableStateCacheTtl*time.Second).Err()
|
||||||
|
if err != nil {
|
||||||
|
// @TODO: Log the error here.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dt.resp.RecordsTotal = len(dt.all)
|
||||||
|
|
||||||
|
//fmt.Println("dt.req.Search.Value ", dt.req.Search.Value )
|
||||||
|
var hasColFilter bool
|
||||||
|
for i := 0; i < len(dt.req.Columns); i++ {
|
||||||
|
cn := dt.req.Columns[i]
|
||||||
|
if !cn.Searchable {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if cn.Search.Value != "" {
|
||||||
|
// fmt.Println("col filter on", cn.Name)
|
||||||
|
hasColFilter = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
filtered := [][]ColumnValue{}
|
||||||
|
for _, l := range dt.all {
|
||||||
|
var skip bool
|
||||||
|
var oneColAtleastMatches bool
|
||||||
|
for i := 0; i < len(dt.req.Columns); i++ {
|
||||||
|
cn := dt.req.Columns[i]
|
||||||
|
|
||||||
|
if cn.Name == dt.storeFilteredFieldName {
|
||||||
|
dt.filteredFieldValues = append(dt.filteredFieldValues, l[i].Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !cn.Searchable {
|
||||||
|
// fmt.Println("col ", cn.Name, "is not searchable skipping")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if cn.Search.Value != "" {
|
||||||
|
if cn.Search.Regexp != nil {
|
||||||
|
//fmt.Println("col regex", cn.Search.Value, "->>>>", l[i].Value)
|
||||||
|
|
||||||
|
if !cn.Search.Regexp.MatchString(l[i].Value) {
|
||||||
|
//fmt.Println("-> no match")
|
||||||
|
skip = true
|
||||||
|
if dt.req.Search.Value == "" {
|
||||||
|
// only skip if not full search
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var match bool
|
||||||
|
if !dt.caseSensitive {
|
||||||
|
match = strings.Contains(
|
||||||
|
strings.ToLower(l[i].Value),
|
||||||
|
strings.ToLower(cn.Search.Value))
|
||||||
|
} else {
|
||||||
|
match = strings.Contains(l[i].Value, cn.Search.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if !match {
|
||||||
|
//fmt.Println("-> no match")
|
||||||
|
skip = true
|
||||||
|
if dt.req.Search.Value == "" {
|
||||||
|
// only skip if not full search
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if dt.req.Search.Value != "" {
|
||||||
|
if dt.req.Search.Regexp != nil {
|
||||||
|
//fmt.Println("req regex", cn.Search.Value, "->>>>", l[i].Value)
|
||||||
|
|
||||||
|
if dt.req.Search.Regexp.MatchString(l[i].Value) {
|
||||||
|
// fmt.Println("-> match")
|
||||||
|
oneColAtleastMatches = true
|
||||||
|
if !hasColFilter {
|
||||||
|
// only skip if no column filter
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if strings.Contains(l[i].Value, dt.req.Search.Value) {
|
||||||
|
// fmt.Println("-> match")
|
||||||
|
oneColAtleastMatches = true
|
||||||
|
if !hasColFilter {
|
||||||
|
// only skip if no column filter
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasColFilter && dt.req.Search.Value != "" {
|
||||||
|
if !skip && oneColAtleastMatches {
|
||||||
|
filtered = append(filtered, l)
|
||||||
|
}
|
||||||
|
} else if hasColFilter {
|
||||||
|
if !skip {
|
||||||
|
filtered = append(filtered, l)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if dt.req.Search.Value != "" {
|
||||||
|
if oneColAtleastMatches {
|
||||||
|
filtered = append(filtered, l)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
filtered = append(filtered, l)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dt.resp.RecordsFiltered = len(filtered)
|
||||||
|
|
||||||
|
for idx, l := range filtered {
|
||||||
|
if dt.req.Start > 0 && idx < dt.req.Start {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fl := []string{}
|
||||||
|
for _, lv := range l {
|
||||||
|
if lv.Formatted != "" {
|
||||||
|
fl = append(fl, lv.Formatted)
|
||||||
|
} else {
|
||||||
|
fl = append(fl, lv.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dt.resp.Data = append(dt.resp.Data, fl)
|
||||||
|
if dt.req.Length > 0 && len(dt.resp.Data) >= dt.req.Length {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rendered, web.RespondJson(dt.ctx, dt.w, dt.resp, http.StatusOK)
|
||||||
|
}
|
@ -14,7 +14,7 @@ type SignupRequest struct {
|
|||||||
|
|
||||||
// SignupAccount defined the details needed for account.
|
// SignupAccount defined the details needed for account.
|
||||||
type SignupAccount struct {
|
type SignupAccount struct {
|
||||||
Name string `json:"name" validate:"required,unique" example:"Company {RANDOM_UUID}"`
|
Name string `json:"name" validate:"required,unique-name" example:"Company {RANDOM_UUID}"`
|
||||||
Address1 string `json:"address1" validate:"required" example:"221 Tatitlek Ave"`
|
Address1 string `json:"address1" validate:"required" example:"221 Tatitlek Ave"`
|
||||||
Address2 string `json:"address2" validate:"omitempty" example:"Box #1832"`
|
Address2 string `json:"address2" validate:"omitempty" example:"Box #1832"`
|
||||||
City string `json:"city" validate:"required" example:"Valdez"`
|
City string `json:"city" validate:"required" example:"Valdez"`
|
||||||
@ -28,7 +28,7 @@ type SignupAccount struct {
|
|||||||
type SignupUser struct {
|
type SignupUser struct {
|
||||||
FirstName string `json:"first_name" validate:"required" example:"Gabi"`
|
FirstName string `json:"first_name" validate:"required" example:"Gabi"`
|
||||||
LastName string `json:"last_name" validate:"required" example:"May"`
|
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-email" 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:"required,eqfield=Password" example:"SecretString"`
|
PasswordConfirm string `json:"password_confirm" validate:"required,eqfield=Password" example:"SecretString"`
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@ package signup
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"geeks-accelerator/oss/saas-starter-kit/internal/account"
|
"geeks-accelerator/oss/saas-starter-kit/internal/account"
|
||||||
@ -15,6 +14,63 @@ import (
|
|||||||
"gopkg.in/go-playground/validator.v9"
|
"gopkg.in/go-playground/validator.v9"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type ctxKeyTagUniqueName int
|
||||||
|
|
||||||
|
const KeyTagUniqueName ctxKeyTagUniqueName = 1
|
||||||
|
|
||||||
|
|
||||||
|
type ctxKeyTagUniqueEmail int
|
||||||
|
|
||||||
|
const KeyTagUniqueEmail ctxKeyTagUniqueEmail = 1
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// validate holds the settings and caches for validating request struct values.
|
||||||
|
var validate *validator.Validate
|
||||||
|
|
||||||
|
// Validator returns the current init validator.
|
||||||
|
func Validator() *validator.Validate {
|
||||||
|
if validate == nil {
|
||||||
|
validate = webcontext.Validator()
|
||||||
|
|
||||||
|
validate.RegisterValidationCtx("unique-name", func(ctx context.Context, fl validator.FieldLevel) bool {
|
||||||
|
if fl.Field().String() == "invalid" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
cv := ctx.Value(KeyTagUniqueName)
|
||||||
|
if cv == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, ok := cv.(bool); ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
validate.RegisterValidationCtx("unique-email", func(ctx context.Context, fl validator.FieldLevel) bool {
|
||||||
|
if fl.Field().String() == "invalid" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
cv := ctx.Value(KeyTagUniqueEmail)
|
||||||
|
if cv == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, ok := cv.(bool); ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return validate
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// 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
|
||||||
// both records with a new user_account entry.
|
// both records with a new user_account entry.
|
||||||
func Signup(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req SignupRequest, now time.Time) (*SignupResult, error) {
|
func Signup(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req SignupRequest, now time.Time) (*SignupResult, error) {
|
||||||
@ -26,37 +82,17 @@ func Signup(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req Signup
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
ctx = context.WithValue(ctx, KeyTagUniqueEmail, uniqEmail)
|
||||||
|
|
||||||
// Validate the account name is unique in the database.
|
// Validate the account name is unique in the database.
|
||||||
uniqName, err := account.UniqueName(ctx, dbConn, req.Account.Name, "")
|
uniqName, err := account.UniqueName(ctx, dbConn, req.Account.Name, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
ctx = context.WithValue(ctx, KeyTagUniqueName, uniqName)
|
||||||
f := func(fl validator.FieldLevel) bool {
|
|
||||||
if fl.Field().String() == "invalid" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
fieldName := strings.Trim(fl.FieldName(), "{}")
|
|
||||||
|
|
||||||
var uniq bool
|
|
||||||
switch fieldName {
|
|
||||||
case "Name", "name":
|
|
||||||
uniq = uniqName
|
|
||||||
case "Email", "email":
|
|
||||||
uniq = uniqEmail
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return uniq
|
|
||||||
}
|
|
||||||
|
|
||||||
v := webcontext.Validator()
|
|
||||||
v.RegisterValidation("unique", f)
|
|
||||||
|
|
||||||
// Validate the request.
|
// Validate the request.
|
||||||
err = v.Struct(req)
|
err = Validator().StructCtx(ctx, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -254,3 +254,93 @@ func (s UserAccountRoles) Value() (driver.Value, error) {
|
|||||||
|
|
||||||
return arr.Value()
|
return arr.Value()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// User represents someone with access to our system.
|
||||||
|
type User struct {
|
||||||
|
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"`
|
||||||
|
Timezone string `json:"timezone" validate:"omitempty" example:"America/Anchorage"`
|
||||||
|
AccountID string `json:"account_id" validate:"required,uuid" example:"c4653bf9-5978-48b7-89c5-95704aebb7e2"`
|
||||||
|
Roles UserAccountRoles `json:"roles" validate:"required,dive,oneof=admin user" enums:"admin,user" swaggertype:"array,string" example:"admin"`
|
||||||
|
Status UserAccountStatus `json:"status" validate:"omitempty,oneof=active invited disabled" enums:"active,invited,disabled" swaggertype:"string" example:"active"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
ArchivedAt *pq.NullTime `json:"archived_at,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserResponse represents someone with access to our system that is returned for display.
|
||||||
|
type UserResponse struct {
|
||||||
|
ID string `json:"id" example:"d69bdef7-173f-4d29-b52c-3edc60baf6a2"`
|
||||||
|
Name string `json:"name" example:"Gabi"`
|
||||||
|
FirstName string `json:"first_name" example:"Gabi"`
|
||||||
|
LastName string `json:"last_name" example:"May"`
|
||||||
|
Email string `json:"email" example:"gabi@geeksinthewoods.com"`
|
||||||
|
Timezone string `json:"timezone" example:"America/Anchorage"`
|
||||||
|
AccountID string `json:"account_id" example:"c4653bf9-5978-48b7-89c5-95704aebb7e2"`
|
||||||
|
Roles UserAccountRoles `json:"roles" validate:"required,dive,oneof=admin user" enums:"admin,user" swaggertype:"array,string" example:"admin"`
|
||||||
|
Status web.EnumResponse `json:"status"` // Status is enum with values [active, invited, disabled].
|
||||||
|
CreatedAt web.TimeResponse `json:"created_at"` // CreatedAt contains multiple format options for display.
|
||||||
|
UpdatedAt web.TimeResponse `json:"updated_at"` // UpdatedAt contains multiple format options for display.
|
||||||
|
ArchivedAt *web.TimeResponse `json:"archived_at,omitempty"` // ArchivedAt contains multiple format options for display.
|
||||||
|
Gravatar web.GravatarResponse `json:"gravatar"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response transforms User and UserResponse that is used for display.
|
||||||
|
// Additional filtering by context values or translations could be applied.
|
||||||
|
func (m *User) Response(ctx context.Context) *UserResponse {
|
||||||
|
if m == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
r := &UserResponse{
|
||||||
|
ID: m.ID,
|
||||||
|
Name: m.Name,
|
||||||
|
FirstName: m.FirstName,
|
||||||
|
LastName: m.LastName,
|
||||||
|
Email: m.Email,
|
||||||
|
Timezone: m.Timezone,
|
||||||
|
AccountID: m.AccountID,
|
||||||
|
Roles: m.Roles,
|
||||||
|
Status: web.NewEnumResponse(ctx, m.Status, UserAccountStatus_Values),
|
||||||
|
CreatedAt: web.NewTimeResponse(ctx, m.CreatedAt),
|
||||||
|
UpdatedAt: web.NewTimeResponse(ctx, m.UpdatedAt),
|
||||||
|
Gravatar: web.NewGravatarResponse(ctx, m.Email),
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.ArchivedAt != nil && !m.ArchivedAt.Time.IsZero() {
|
||||||
|
at := web.NewTimeResponse(ctx, m.ArchivedAt.Time)
|
||||||
|
r.ArchivedAt = &at
|
||||||
|
}
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// Users a list of Users.
|
||||||
|
type Users []*User
|
||||||
|
|
||||||
|
// Response transforms a list of Users to a list of UserResponses.
|
||||||
|
func (m *Users) Response(ctx context.Context) []*UserResponse {
|
||||||
|
var l []*UserResponse
|
||||||
|
if m != nil && len(*m) > 0 {
|
||||||
|
for _, n := range *m {
|
||||||
|
l = append(l, n.Response(ctx))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserFindByAccountRequest defines the possible options to search for users by account ID.
|
||||||
|
// By default archived users will be excluded from response.
|
||||||
|
type UserFindByAccountRequest struct {
|
||||||
|
AccountID string `json:"account_id" validate:"required,uuid" example:"c4653bf9-5978-48b7-89c5-95704aebb7e2"`
|
||||||
|
Where string `json:"where" example:"name = ? and email = ?"`
|
||||||
|
Args []interface{} `json:"args" swaggertype:"array,string" example:"Company Name,gabi.may@geeksinthewoods.com"`
|
||||||
|
Order []string `json:"order" example:"created_at desc"`
|
||||||
|
Limit *uint `json:"limit" example:"10"`
|
||||||
|
Offset *uint `json:"offset" example:"20"`
|
||||||
|
IncludeArchived bool `json:"include-archived" example:"false"`
|
||||||
|
}
|
||||||
|
27
internal/user_account/user.go
Normal file
27
internal/user_account/user.go
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package user_account
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext"
|
||||||
|
|
||||||
|
"geeks-accelerator/oss/saas-starter-kit/internal/platform/auth"
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UserFindByAccount lists all the users for a given account ID.
|
||||||
|
func UserFindByAccount(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req UserFindByAccountRequest) (Users, error) {
|
||||||
|
span, ctx := tracer.StartSpanFromContext(ctx, "internal.user_account.UserFind")
|
||||||
|
defer span.Finish()
|
||||||
|
|
||||||
|
v := webcontext.Validator()
|
||||||
|
|
||||||
|
// Validate the request.
|
||||||
|
err := v.StructCtx(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return nil , nil
|
||||||
|
}
|
Reference in New Issue
Block a user