You've already forked golang-saas-starter-kit
mirror of
https://github.com/raseels-repos/golang-saas-starter-kit.git
synced 2025-06-15 00:15:15 +02:00
checkpoint
This commit is contained in:
@ -30,7 +30,6 @@ type Account struct {
|
|||||||
// @Param id path string true "Account ID"
|
// @Param id path string true "Account ID"
|
||||||
// @Success 200 {object} account.AccountResponse
|
// @Success 200 {object} account.AccountResponse
|
||||||
// @Failure 400 {object} web.ErrorResponse
|
// @Failure 400 {object} web.ErrorResponse
|
||||||
// @Failure 403 {object} web.ErrorResponse
|
|
||||||
// @Failure 404 {object} web.ErrorResponse
|
// @Failure 404 {object} web.ErrorResponse
|
||||||
// @Failure 500 {object} web.ErrorResponse
|
// @Failure 500 {object} web.ErrorResponse
|
||||||
// @Router /accounts/{id} [get]
|
// @Router /accounts/{id} [get]
|
||||||
@ -40,24 +39,23 @@ func (a *Account) Read(ctx context.Context, w http.ResponseWriter, r *http.Reque
|
|||||||
return errors.New("claims missing from context")
|
return errors.New("claims missing from context")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle included-archived query value if set.
|
||||||
var includeArchived bool
|
var includeArchived bool
|
||||||
if qv := r.URL.Query().Get("include-archived"); qv != "" {
|
if v := r.URL.Query().Get("included-archived"); v != "" {
|
||||||
var err error
|
b, err := strconv.ParseBool(v)
|
||||||
includeArchived, err = strconv.ParseBool(qv)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrapf(err, "Invalid value for include-archived : %s", qv)
|
err = errors.WithMessagef(err, "unable to parse %s as boolean for included-archived param", v)
|
||||||
|
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusBadRequest))
|
||||||
}
|
}
|
||||||
|
includeArchived = b
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := account.Read(ctx, claims, a.MasterDB, params["id"], includeArchived)
|
res, err := account.Read(ctx, claims, a.MasterDB, params["id"], includeArchived)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch err {
|
cause := errors.Cause(err)
|
||||||
case account.ErrInvalidID:
|
switch cause {
|
||||||
return web.NewRequestError(err, http.StatusBadRequest)
|
|
||||||
case account.ErrNotFound:
|
case account.ErrNotFound:
|
||||||
return web.NewRequestError(err, http.StatusNotFound)
|
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusNotFound))
|
||||||
case account.ErrForbidden:
|
|
||||||
return web.NewRequestError(err, http.StatusForbidden)
|
|
||||||
default:
|
default:
|
||||||
return errors.Wrapf(err, "ID: %s", params["id"])
|
return errors.Wrapf(err, "ID: %s", params["id"])
|
||||||
}
|
}
|
||||||
@ -74,10 +72,9 @@ func (a *Account) Read(ctx context.Context, w http.ResponseWriter, r *http.Reque
|
|||||||
// @Produce json
|
// @Produce json
|
||||||
// @Security OAuth2Password
|
// @Security OAuth2Password
|
||||||
// @Param data body account.AccountUpdateRequest true "Update fields"
|
// @Param data body account.AccountUpdateRequest true "Update fields"
|
||||||
// @Success 201
|
// @Success 204
|
||||||
// @Failure 400 {object} web.ErrorResponse
|
// @Failure 400 {object} web.ErrorResponse
|
||||||
// @Failure 403 {object} web.ErrorResponse
|
// @Failure 403 {object} web.ErrorResponse
|
||||||
// @Failure 404 {object} web.ErrorResponse
|
|
||||||
// @Failure 500 {object} web.ErrorResponse
|
// @Failure 500 {object} web.ErrorResponse
|
||||||
// @Router /accounts [patch]
|
// @Router /accounts [patch]
|
||||||
func (a *Account) Update(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
func (a *Account) Update(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||||
@ -93,31 +90,25 @@ func (a *Account) Update(ctx context.Context, w http.ResponseWriter, r *http.Req
|
|||||||
|
|
||||||
var req account.AccountUpdateRequest
|
var req account.AccountUpdateRequest
|
||||||
if err := web.Decode(r, &req); err != nil {
|
if err := web.Decode(r, &req); err != nil {
|
||||||
err = errors.WithStack(err)
|
if _, ok := errors.Cause(err).(*web.Error); !ok {
|
||||||
|
err = web.NewRequestError(err, http.StatusBadRequest)
|
||||||
_, ok := err.(validator.ValidationErrors)
|
|
||||||
if ok {
|
|
||||||
return web.NewRequestError(err, http.StatusBadRequest)
|
|
||||||
}
|
}
|
||||||
return err
|
return web.RespondJsonError(ctx, w, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err := account.Update(ctx, claims, a.MasterDB, req, v.Now)
|
err := account.Update(ctx, claims, a.MasterDB, req, v.Now)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch err {
|
cause := errors.Cause(err)
|
||||||
case account.ErrInvalidID:
|
switch cause {
|
||||||
return web.NewRequestError(err, http.StatusBadRequest)
|
|
||||||
case account.ErrNotFound:
|
|
||||||
return web.NewRequestError(err, http.StatusNotFound)
|
|
||||||
case account.ErrForbidden:
|
case account.ErrForbidden:
|
||||||
return web.NewRequestError(err, http.StatusForbidden)
|
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusForbidden))
|
||||||
default:
|
default:
|
||||||
_, ok := err.(validator.ValidationErrors)
|
_, ok := cause.(validator.ValidationErrors)
|
||||||
if ok {
|
if ok {
|
||||||
return web.NewRequestError(err, http.StatusBadRequest)
|
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusBadRequest))
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors.Wrapf(err, "Id: %s Account: %+v", params["id"], &req)
|
return errors.Wrapf(err, "Id: %s Account: %+v", req.ID, &req)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,7 +50,7 @@ func (p *Project) Find(ctx context.Context, w http.ResponseWriter, r *http.Reque
|
|||||||
if v := r.URL.Query().Get("where"); v != "" {
|
if v := r.URL.Query().Get("where"); v != "" {
|
||||||
where, args, err := web.ExtractWhereArgs(v)
|
where, args, err := web.ExtractWhereArgs(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return web.NewRequestError(err, http.StatusBadRequest)
|
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusBadRequest))
|
||||||
}
|
}
|
||||||
req.Where = &where
|
req.Where = &where
|
||||||
req.Args = args
|
req.Args = args
|
||||||
@ -71,7 +71,7 @@ func (p *Project) Find(ctx context.Context, w http.ResponseWriter, r *http.Reque
|
|||||||
l, err := strconv.Atoi(v)
|
l, err := strconv.Atoi(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = errors.WithMessagef(err, "unable to parse %s as int for limit param", v)
|
err = errors.WithMessagef(err, "unable to parse %s as int for limit param", v)
|
||||||
return web.NewRequestError(err, http.StatusBadRequest)
|
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusBadRequest))
|
||||||
}
|
}
|
||||||
ul := uint(l)
|
ul := uint(l)
|
||||||
req.Limit = &ul
|
req.Limit = &ul
|
||||||
@ -82,22 +82,29 @@ func (p *Project) Find(ctx context.Context, w http.ResponseWriter, r *http.Reque
|
|||||||
l, err := strconv.Atoi(v)
|
l, err := strconv.Atoi(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = errors.WithMessagef(err, "unable to parse %s as int for offset param", v)
|
err = errors.WithMessagef(err, "unable to parse %s as int for offset param", v)
|
||||||
return web.NewRequestError(err, http.StatusBadRequest)
|
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusBadRequest))
|
||||||
}
|
}
|
||||||
ul := uint(l)
|
ul := uint(l)
|
||||||
req.Limit = &ul
|
req.Limit = &ul
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle order query value if set.
|
// Handle include-archive query value if set.
|
||||||
if v := r.URL.Query().Get("included-archived"); v != "" {
|
if v := r.URL.Query().Get("included-archived"); v != "" {
|
||||||
b, err := strconv.ParseBool(v)
|
b, err := strconv.ParseBool(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = errors.WithMessagef(err, "unable to parse %s as boolean for included-archived param", v)
|
err = errors.WithMessagef(err, "unable to parse %s as boolean for included-archived param", v)
|
||||||
return web.NewRequestError(err, http.StatusBadRequest)
|
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusBadRequest))
|
||||||
}
|
}
|
||||||
req.IncludedArchived = b
|
req.IncludedArchived = b
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//if err := web.Decode(r, &req); err != nil {
|
||||||
|
// if _, ok := errors.Cause(err).(*web.Error); !ok {
|
||||||
|
// err = web.NewRequestError(err, http.StatusBadRequest)
|
||||||
|
// }
|
||||||
|
// return web.RespondJsonError(ctx, w, err)
|
||||||
|
//}
|
||||||
|
|
||||||
res, err := project.Find(ctx, claims, p.MasterDB, req)
|
res, err := project.Find(ctx, claims, p.MasterDB, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -121,7 +128,6 @@ func (p *Project) Find(ctx context.Context, w http.ResponseWriter, r *http.Reque
|
|||||||
// @Param id path string true "Project ID"
|
// @Param id path string true "Project ID"
|
||||||
// @Success 200 {object} project.ProjectResponse
|
// @Success 200 {object} project.ProjectResponse
|
||||||
// @Failure 400 {object} web.ErrorResponse
|
// @Failure 400 {object} web.ErrorResponse
|
||||||
// @Failure 403 {object} web.ErrorResponse
|
|
||||||
// @Failure 404 {object} web.ErrorResponse
|
// @Failure 404 {object} web.ErrorResponse
|
||||||
// @Failure 500 {object} web.ErrorResponse
|
// @Failure 500 {object} web.ErrorResponse
|
||||||
// @Router /projects/{id} [get]
|
// @Router /projects/{id} [get]
|
||||||
@ -131,24 +137,23 @@ func (p *Project) Read(ctx context.Context, w http.ResponseWriter, r *http.Reque
|
|||||||
return errors.New("claims missing from context")
|
return errors.New("claims missing from context")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle included-archived query value if set.
|
||||||
var includeArchived bool
|
var includeArchived bool
|
||||||
if qv := r.URL.Query().Get("include-archived"); qv != "" {
|
if v := r.URL.Query().Get("included-archived"); v != "" {
|
||||||
var err error
|
b, err := strconv.ParseBool(v)
|
||||||
includeArchived, err = strconv.ParseBool(qv)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrapf(err, "Invalid value for include-archived : %s", qv)
|
err = errors.WithMessagef(err, "unable to parse %s as boolean for included-archived param", v)
|
||||||
|
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusBadRequest))
|
||||||
}
|
}
|
||||||
|
includeArchived = b
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := project.Read(ctx, claims, p.MasterDB, params["id"], includeArchived)
|
res, err := project.Read(ctx, claims, p.MasterDB, params["id"], includeArchived)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch err {
|
cause := errors.Cause(err)
|
||||||
case project.ErrInvalidID:
|
switch cause {
|
||||||
return web.NewRequestError(err, http.StatusBadRequest)
|
|
||||||
case project.ErrNotFound:
|
case project.ErrNotFound:
|
||||||
return web.NewRequestError(err, http.StatusNotFound)
|
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusNotFound))
|
||||||
case project.ErrForbidden:
|
|
||||||
return web.NewRequestError(err, http.StatusForbidden)
|
|
||||||
default:
|
default:
|
||||||
return errors.Wrapf(err, "ID: %s", params["id"])
|
return errors.Wrapf(err, "ID: %s", params["id"])
|
||||||
}
|
}
|
||||||
@ -165,7 +170,7 @@ func (p *Project) Read(ctx context.Context, w http.ResponseWriter, r *http.Reque
|
|||||||
// @Produce json
|
// @Produce json
|
||||||
// @Security OAuth2Password
|
// @Security OAuth2Password
|
||||||
// @Param data body project.ProjectCreateRequest true "Project details"
|
// @Param data body project.ProjectCreateRequest true "Project details"
|
||||||
// @Success 200 {object} project.ProjectResponse
|
// @Success 201 {object} project.ProjectResponse
|
||||||
// @Failure 400 {object} web.ErrorResponse
|
// @Failure 400 {object} web.ErrorResponse
|
||||||
// @Failure 403 {object} web.ErrorResponse
|
// @Failure 403 {object} web.ErrorResponse
|
||||||
// @Failure 404 {object} web.ErrorResponse
|
// @Failure 404 {object} web.ErrorResponse
|
||||||
@ -184,24 +189,22 @@ func (p *Project) Create(ctx context.Context, w http.ResponseWriter, r *http.Req
|
|||||||
|
|
||||||
var req project.ProjectCreateRequest
|
var req project.ProjectCreateRequest
|
||||||
if err := web.Decode(r, &req); err != nil {
|
if err := web.Decode(r, &req); err != nil {
|
||||||
err = errors.WithStack(err)
|
if _, ok := errors.Cause(err).(*web.Error); !ok {
|
||||||
|
err = web.NewRequestError(err, http.StatusBadRequest)
|
||||||
_, ok := err.(validator.ValidationErrors)
|
|
||||||
if ok {
|
|
||||||
return web.NewRequestError(err, http.StatusBadRequest)
|
|
||||||
}
|
}
|
||||||
return err
|
return web.RespondJsonError(ctx, w, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := project.Create(ctx, claims, p.MasterDB, req, v.Now)
|
res, err := project.Create(ctx, claims, p.MasterDB, req, v.Now)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch err {
|
cause := errors.Cause(err)
|
||||||
|
switch cause {
|
||||||
case project.ErrForbidden:
|
case project.ErrForbidden:
|
||||||
return web.NewRequestError(err, http.StatusForbidden)
|
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusForbidden))
|
||||||
default:
|
default:
|
||||||
_, ok := err.(validator.ValidationErrors)
|
_, ok := cause.(validator.ValidationErrors)
|
||||||
if ok {
|
if ok {
|
||||||
return web.NewRequestError(err, http.StatusBadRequest)
|
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusBadRequest))
|
||||||
}
|
}
|
||||||
return errors.Wrapf(err, "Project: %+v", &req)
|
return errors.Wrapf(err, "Project: %+v", &req)
|
||||||
}
|
}
|
||||||
@ -218,10 +221,9 @@ func (p *Project) Create(ctx context.Context, w http.ResponseWriter, r *http.Req
|
|||||||
// @Produce json
|
// @Produce json
|
||||||
// @Security OAuth2Password
|
// @Security OAuth2Password
|
||||||
// @Param data body project.ProjectUpdateRequest true "Update fields"
|
// @Param data body project.ProjectUpdateRequest true "Update fields"
|
||||||
// @Success 201
|
// @Success 204
|
||||||
// @Failure 400 {object} web.ErrorResponse
|
// @Failure 400 {object} web.ErrorResponse
|
||||||
// @Failure 403 {object} web.ErrorResponse
|
// @Failure 403 {object} web.ErrorResponse
|
||||||
// @Failure 404 {object} web.ErrorResponse
|
|
||||||
// @Failure 500 {object} web.ErrorResponse
|
// @Failure 500 {object} web.ErrorResponse
|
||||||
// @Router /projects [patch]
|
// @Router /projects [patch]
|
||||||
func (p *Project) Update(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
func (p *Project) Update(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||||
@ -237,29 +239,24 @@ func (p *Project) Update(ctx context.Context, w http.ResponseWriter, r *http.Req
|
|||||||
|
|
||||||
var req project.ProjectUpdateRequest
|
var req project.ProjectUpdateRequest
|
||||||
if err := web.Decode(r, &req); err != nil {
|
if err := web.Decode(r, &req); err != nil {
|
||||||
err = errors.WithStack(err)
|
if _, ok := errors.Cause(err).(*web.Error); !ok {
|
||||||
|
err = web.NewRequestError(err, http.StatusBadRequest)
|
||||||
_, ok := err.(validator.ValidationErrors)
|
|
||||||
if ok {
|
|
||||||
return web.NewRequestError(err, http.StatusBadRequest)
|
|
||||||
}
|
}
|
||||||
return err
|
return web.RespondJsonError(ctx, w, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err := project.Update(ctx, claims, p.MasterDB, req, v.Now)
|
err := project.Update(ctx, claims, p.MasterDB, req, v.Now)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch err {
|
cause := errors.Cause(err)
|
||||||
case project.ErrInvalidID:
|
switch cause {
|
||||||
return web.NewRequestError(err, http.StatusBadRequest)
|
|
||||||
case project.ErrNotFound:
|
|
||||||
return web.NewRequestError(err, http.StatusNotFound)
|
|
||||||
case project.ErrForbidden:
|
case project.ErrForbidden:
|
||||||
return web.NewRequestError(err, http.StatusForbidden)
|
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusForbidden))
|
||||||
default:
|
default:
|
||||||
_, ok := err.(validator.ValidationErrors)
|
_, ok := cause.(validator.ValidationErrors)
|
||||||
if ok {
|
if ok {
|
||||||
return web.NewRequestError(err, http.StatusBadRequest)
|
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusBadRequest))
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors.Wrapf(err, "ID: %s Update: %+v", req.ID, req)
|
return errors.Wrapf(err, "ID: %s Update: %+v", req.ID, req)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -275,7 +272,7 @@ func (p *Project) Update(ctx context.Context, w http.ResponseWriter, r *http.Req
|
|||||||
// @Produce json
|
// @Produce json
|
||||||
// @Security OAuth2Password
|
// @Security OAuth2Password
|
||||||
// @Param data body project.ProjectArchiveRequest true "Update fields"
|
// @Param data body project.ProjectArchiveRequest true "Update fields"
|
||||||
// @Success 201
|
// @Success 204
|
||||||
// @Failure 400 {object} web.ErrorResponse
|
// @Failure 400 {object} web.ErrorResponse
|
||||||
// @Failure 403 {object} web.ErrorResponse
|
// @Failure 403 {object} web.ErrorResponse
|
||||||
// @Failure 404 {object} web.ErrorResponse
|
// @Failure 404 {object} web.ErrorResponse
|
||||||
@ -294,28 +291,24 @@ func (p *Project) Archive(ctx context.Context, w http.ResponseWriter, r *http.Re
|
|||||||
|
|
||||||
var req project.ProjectArchiveRequest
|
var req project.ProjectArchiveRequest
|
||||||
if err := web.Decode(r, &req); err != nil {
|
if err := web.Decode(r, &req); err != nil {
|
||||||
err = errors.WithStack(err)
|
if _, ok := errors.Cause(err).(*web.Error); !ok {
|
||||||
|
err = web.NewRequestError(err, http.StatusBadRequest)
|
||||||
_, ok := err.(validator.ValidationErrors)
|
|
||||||
if ok {
|
|
||||||
return web.NewRequestError(err, http.StatusBadRequest)
|
|
||||||
}
|
}
|
||||||
return err
|
return web.RespondJsonError(ctx, w, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err := project.Archive(ctx, claims, p.MasterDB, req, v.Now)
|
err := project.Archive(ctx, claims, p.MasterDB, req, v.Now)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch err {
|
cause := errors.Cause(err)
|
||||||
case project.ErrInvalidID:
|
switch cause {
|
||||||
return web.NewRequestError(err, http.StatusBadRequest)
|
|
||||||
case project.ErrNotFound:
|
case project.ErrNotFound:
|
||||||
return web.NewRequestError(err, http.StatusNotFound)
|
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusNotFound))
|
||||||
case project.ErrForbidden:
|
case project.ErrForbidden:
|
||||||
return web.NewRequestError(err, http.StatusForbidden)
|
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusForbidden))
|
||||||
default:
|
default:
|
||||||
_, ok := err.(validator.ValidationErrors)
|
_, ok := cause.(validator.ValidationErrors)
|
||||||
if ok {
|
if ok {
|
||||||
return web.NewRequestError(err, http.StatusBadRequest)
|
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusBadRequest))
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors.Wrapf(err, "Id: %s", req.ID)
|
return errors.Wrapf(err, "Id: %s", req.ID)
|
||||||
@ -333,7 +326,7 @@ func (p *Project) Archive(ctx context.Context, w http.ResponseWriter, r *http.Re
|
|||||||
// @Produce json
|
// @Produce json
|
||||||
// @Security OAuth2Password
|
// @Security OAuth2Password
|
||||||
// @Param id path string true "Project ID"
|
// @Param id path string true "Project ID"
|
||||||
// @Success 201
|
// @Success 204
|
||||||
// @Failure 400 {object} web.ErrorResponse
|
// @Failure 400 {object} web.ErrorResponse
|
||||||
// @Failure 403 {object} web.ErrorResponse
|
// @Failure 403 {object} web.ErrorResponse
|
||||||
// @Failure 404 {object} web.ErrorResponse
|
// @Failure 404 {object} web.ErrorResponse
|
||||||
@ -347,13 +340,12 @@ func (p *Project) Delete(ctx context.Context, w http.ResponseWriter, r *http.Req
|
|||||||
|
|
||||||
err := project.Delete(ctx, claims, p.MasterDB, params["id"])
|
err := project.Delete(ctx, claims, p.MasterDB, params["id"])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch err {
|
cause := errors.Cause(err)
|
||||||
case project.ErrInvalidID:
|
switch cause {
|
||||||
return web.NewRequestError(err, http.StatusBadRequest)
|
|
||||||
case project.ErrNotFound:
|
case project.ErrNotFound:
|
||||||
return web.NewRequestError(err, http.StatusNotFound)
|
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusNotFound))
|
||||||
case project.ErrForbidden:
|
case project.ErrForbidden:
|
||||||
return web.NewRequestError(err, http.StatusForbidden)
|
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusForbidden))
|
||||||
default:
|
default:
|
||||||
return errors.Wrapf(err, "Id: %s", params["id"])
|
return errors.Wrapf(err, "Id: %s", params["id"])
|
||||||
}
|
}
|
||||||
|
@ -30,12 +30,11 @@ func API(shutdown chan os.Signal, log *log.Logger, masterDB *sqlx.DB, redis *red
|
|||||||
MasterDB: masterDB,
|
MasterDB: masterDB,
|
||||||
TokenGenerator: authenticator,
|
TokenGenerator: authenticator,
|
||||||
}
|
}
|
||||||
|
|
||||||
app.Handle("GET", "/v1/users", u.Find, mid.Authenticate(authenticator))
|
app.Handle("GET", "/v1/users", u.Find, mid.Authenticate(authenticator))
|
||||||
app.Handle("POST", "/v1/users", u.Create, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
|
app.Handle("POST", "/v1/users", u.Create, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
|
||||||
app.Handle("GET", "/v1/users/:id", u.Read, mid.Authenticate(authenticator))
|
app.Handle("GET", "/v1/users/:id", u.Read, mid.Authenticate(authenticator))
|
||||||
app.Handle("PATCH", "/v1/users", u.Update, mid.Authenticate(authenticator))
|
app.Handle("PATCH", "/v1/users", u.Update, mid.Authenticate(authenticator))
|
||||||
app.Handle("PATCH", "/v1/users/password", u.UpdatePassword, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
|
app.Handle("PATCH", "/v1/users/password", u.UpdatePassword, mid.Authenticate(authenticator))
|
||||||
app.Handle("PATCH", "/v1/users/archive", u.Archive, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
|
app.Handle("PATCH", "/v1/users/archive", u.Archive, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
|
||||||
app.Handle("DELETE", "/v1/users/:id", u.Delete, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
|
app.Handle("DELETE", "/v1/users/:id", u.Delete, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
|
||||||
app.Handle("PATCH", "/v1/users/switch-account/:account_id", u.SwitchAccount, mid.Authenticate(authenticator))
|
app.Handle("PATCH", "/v1/users/switch-account/:account_id", u.SwitchAccount, mid.Authenticate(authenticator))
|
||||||
@ -43,6 +42,18 @@ func API(shutdown chan os.Signal, log *log.Logger, masterDB *sqlx.DB, redis *red
|
|||||||
// This route is not authenticated
|
// This route is not authenticated
|
||||||
app.Handle("POST", "/v1/oauth/token", u.Token)
|
app.Handle("POST", "/v1/oauth/token", u.Token)
|
||||||
|
|
||||||
|
|
||||||
|
// Register user account management endpoints.
|
||||||
|
ua := UserAccount{
|
||||||
|
MasterDB: masterDB,
|
||||||
|
}
|
||||||
|
app.Handle("GET", "/v1/user_accounts", ua.Find, mid.Authenticate(authenticator))
|
||||||
|
app.Handle("POST", "/v1/user_accounts", ua.Create, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
|
||||||
|
app.Handle("GET", "/v1/user_accounts/:id", ua.Read, mid.Authenticate(authenticator))
|
||||||
|
app.Handle("PATCH", "/v1/user_accounts", ua.Update, mid.Authenticate(authenticator))
|
||||||
|
app.Handle("PATCH", "/v1/user_accounts/archive", ua.Archive, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
|
||||||
|
app.Handle("DELETE", "/v1/user_accounts", ua.Delete, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
|
||||||
|
|
||||||
// Register account endpoints.
|
// Register account endpoints.
|
||||||
a := Account{
|
a := Account{
|
||||||
MasterDB: masterDB,
|
MasterDB: masterDB,
|
||||||
|
@ -27,9 +27,8 @@ type Signup struct {
|
|||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param data body signup.SignupRequest true "Signup details"
|
// @Param data body signup.SignupRequest true "Signup details"
|
||||||
// @Success 200 {object} signup.SignupResponse
|
// @Success 201 {object} signup.SignupResponse
|
||||||
// @Failure 400 {object} web.ErrorResponse
|
// @Failure 400 {object} web.ErrorResponse
|
||||||
// @Failure 403 {object} web.ErrorResponse
|
|
||||||
// @Failure 500 {object} web.ErrorResponse
|
// @Failure 500 {object} web.ErrorResponse
|
||||||
// @Router /signup [post]
|
// @Router /signup [post]
|
||||||
func (c *Signup) Signup(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
func (c *Signup) Signup(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||||
@ -43,29 +42,26 @@ func (c *Signup) Signup(ctx context.Context, w http.ResponseWriter, r *http.Requ
|
|||||||
|
|
||||||
var req signup.SignupRequest
|
var req signup.SignupRequest
|
||||||
if err := web.Decode(r, &req); err != nil {
|
if err := web.Decode(r, &req); err != nil {
|
||||||
err = errors.WithStack(err)
|
if _, ok := errors.Cause(err).(*web.Error); !ok {
|
||||||
|
err = web.NewRequestError(err, http.StatusBadRequest)
|
||||||
_, ok := err.(validator.ValidationErrors)
|
|
||||||
if ok {
|
|
||||||
return web.NewRequestError(err, http.StatusBadRequest)
|
|
||||||
}
|
}
|
||||||
return err
|
return web.RespondJsonError(ctx, w, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := signup.Signup(ctx, claims, c.MasterDB, req, v.Now)
|
res, err := signup.Signup(ctx, claims, c.MasterDB, req, v.Now)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch err {
|
switch errors.Cause(err) {
|
||||||
case account.ErrForbidden:
|
case account.ErrForbidden:
|
||||||
return web.NewRequestError(err, http.StatusForbidden)
|
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusForbidden))
|
||||||
default:
|
default:
|
||||||
_, ok := err.(validator.ValidationErrors)
|
_, ok := err.(validator.ValidationErrors)
|
||||||
if ok {
|
if ok {
|
||||||
return web.NewRequestError(err, http.StatusBadRequest)
|
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusBadRequest))
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors.Wrapf(err, "Signup: %+v", &req)
|
return errors.Wrapf(err, "Signup: %+v", &req)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return web.RespondJson(ctx, w, res, http.StatusCreated)
|
return web.RespondJson(ctx, w, res.Response(ctx), http.StatusCreated)
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@ package handlers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/user_account"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@ -56,7 +55,7 @@ func (u *User) Find(ctx context.Context, w http.ResponseWriter, r *http.Request,
|
|||||||
if v := r.URL.Query().Get("where"); v != "" {
|
if v := r.URL.Query().Get("where"); v != "" {
|
||||||
where, args, err := web.ExtractWhereArgs(v)
|
where, args, err := web.ExtractWhereArgs(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return web.NewRequestError(err, http.StatusBadRequest)
|
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusBadRequest))
|
||||||
}
|
}
|
||||||
req.Where = &where
|
req.Where = &where
|
||||||
req.Args = args
|
req.Args = args
|
||||||
@ -77,7 +76,7 @@ func (u *User) Find(ctx context.Context, w http.ResponseWriter, r *http.Request,
|
|||||||
l, err := strconv.Atoi(v)
|
l, err := strconv.Atoi(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = errors.WithMessagef(err, "unable to parse %s as int for limit param", v)
|
err = errors.WithMessagef(err, "unable to parse %s as int for limit param", v)
|
||||||
return web.NewRequestError(err, http.StatusBadRequest)
|
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusBadRequest))
|
||||||
}
|
}
|
||||||
ul := uint(l)
|
ul := uint(l)
|
||||||
req.Limit = &ul
|
req.Limit = &ul
|
||||||
@ -88,31 +87,28 @@ func (u *User) Find(ctx context.Context, w http.ResponseWriter, r *http.Request,
|
|||||||
l, err := strconv.Atoi(v)
|
l, err := strconv.Atoi(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = errors.WithMessagef(err, "unable to parse %s as int for offset param", v)
|
err = errors.WithMessagef(err, "unable to parse %s as int for offset param", v)
|
||||||
return web.NewRequestError(err, http.StatusBadRequest)
|
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusBadRequest))
|
||||||
}
|
}
|
||||||
ul := uint(l)
|
ul := uint(l)
|
||||||
req.Limit = &ul
|
req.Limit = &ul
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle order query value if set.
|
// Handle included-archived query value if set.
|
||||||
if v := r.URL.Query().Get("included-archived"); v != "" {
|
if v := r.URL.Query().Get("included-archived"); v != "" {
|
||||||
b, err := strconv.ParseBool(v)
|
b, err := strconv.ParseBool(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = errors.WithMessagef(err, "unable to parse %s as boolean for included-archived param", v)
|
err = errors.WithMessagef(err, "unable to parse %s as boolean for included-archived param", v)
|
||||||
return web.NewRequestError(err, http.StatusBadRequest)
|
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusBadRequest))
|
||||||
}
|
}
|
||||||
req.IncludedArchived = b
|
req.IncludedArchived = b
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := web.Decode(r, &req); err != nil {
|
//if err := web.Decode(r, &req); err != nil {
|
||||||
err = errors.WithStack(err)
|
// if _, ok := errors.Cause(err).(*web.Error); !ok {
|
||||||
|
// err = web.NewRequestError(err, http.StatusBadRequest)
|
||||||
_, ok := err.(validator.ValidationErrors)
|
// }
|
||||||
if ok {
|
// return web.RespondJsonError(ctx, w, err)
|
||||||
return web.NewRequestError(err, http.StatusBadRequest)
|
//}
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := user.Find(ctx, claims, u.MasterDB, req)
|
res, err := user.Find(ctx, claims, u.MasterDB, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -137,7 +133,6 @@ func (u *User) Find(ctx context.Context, w http.ResponseWriter, r *http.Request,
|
|||||||
// @Param id path string true "User ID"
|
// @Param id path string true "User ID"
|
||||||
// @Success 200 {object} user.UserResponse
|
// @Success 200 {object} user.UserResponse
|
||||||
// @Failure 400 {object} web.ErrorResponse
|
// @Failure 400 {object} web.ErrorResponse
|
||||||
// @Failure 403 {object} web.ErrorResponse
|
|
||||||
// @Failure 404 {object} web.ErrorResponse
|
// @Failure 404 {object} web.ErrorResponse
|
||||||
// @Failure 500 {object} web.ErrorResponse
|
// @Failure 500 {object} web.ErrorResponse
|
||||||
// @Router /users/{id} [get]
|
// @Router /users/{id} [get]
|
||||||
@ -147,30 +142,24 @@ func (u *User) Read(ctx context.Context, w http.ResponseWriter, r *http.Request,
|
|||||||
return errors.New("claims missing from context")
|
return errors.New("claims missing from context")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle included-archived query value if set.
|
||||||
var includeArchived bool
|
var includeArchived bool
|
||||||
if qv := r.URL.Query().Get("include-archived"); qv != "" {
|
if v := r.URL.Query().Get("included-archived"); v != "" {
|
||||||
var err error
|
b, err := strconv.ParseBool(v)
|
||||||
includeArchived, err = strconv.ParseBool(qv)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrapf(err, "Invalid value for include-archived : %s", qv)
|
err = errors.WithMessagef(err, "unable to parse %s as boolean for included-archived param", v)
|
||||||
|
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusBadRequest))
|
||||||
}
|
}
|
||||||
|
includeArchived = b
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := user.Read(ctx, claims, u.MasterDB, params["id"], includeArchived)
|
res, err := user.Read(ctx, claims, u.MasterDB, params["id"], includeArchived)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch err {
|
cause := errors.Cause(err)
|
||||||
case user.ErrInvalidID:
|
switch cause {
|
||||||
return web.NewRequestError(err, http.StatusBadRequest)
|
|
||||||
case user.ErrNotFound:
|
case user.ErrNotFound:
|
||||||
return web.NewRequestError(err, http.StatusNotFound)
|
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusNotFound))
|
||||||
case user.ErrForbidden:
|
|
||||||
return web.NewRequestError(err, http.StatusForbidden)
|
|
||||||
default:
|
default:
|
||||||
_, ok := err.(validator.ValidationErrors)
|
|
||||||
if ok {
|
|
||||||
return web.NewRequestError(err, http.StatusBadRequest)
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors.Wrapf(err, "ID: %s", params["id"])
|
return errors.Wrapf(err, "ID: %s", params["id"])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -186,10 +175,9 @@ func (u *User) Read(ctx context.Context, w http.ResponseWriter, r *http.Request,
|
|||||||
// @Produce json
|
// @Produce json
|
||||||
// @Security OAuth2Password
|
// @Security OAuth2Password
|
||||||
// @Param data body user.UserCreateRequest true "User details"
|
// @Param data body user.UserCreateRequest true "User details"
|
||||||
// @Success 200 {object} user.UserResponse
|
// @Success 201 {object} user.UserResponse
|
||||||
// @Failure 400 {object} web.ErrorResponse
|
// @Failure 400 {object} web.ErrorResponse
|
||||||
// @Failure 403 {object} web.ErrorResponse
|
// @Failure 403 {object} web.ErrorResponse
|
||||||
// @Failure 404 {object} web.ErrorResponse
|
|
||||||
// @Failure 500 {object} web.ErrorResponse
|
// @Failure 500 {object} web.ErrorResponse
|
||||||
// @Router /users [post]
|
// @Router /users [post]
|
||||||
func (u *User) Create(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
func (u *User) Create(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||||
@ -205,53 +193,28 @@ func (u *User) Create(ctx context.Context, w http.ResponseWriter, r *http.Reques
|
|||||||
|
|
||||||
var req user.UserCreateRequest
|
var req user.UserCreateRequest
|
||||||
if err := web.Decode(r, &req); err != nil {
|
if err := web.Decode(r, &req); err != nil {
|
||||||
err = errors.WithStack(err)
|
if _, ok := errors.Cause(err).(*web.Error); !ok {
|
||||||
|
err = web.NewRequestError(err, http.StatusBadRequest)
|
||||||
_, ok := err.(validator.ValidationErrors)
|
|
||||||
if ok {
|
|
||||||
return web.NewRequestError(err, http.StatusBadRequest)
|
|
||||||
}
|
}
|
||||||
return err
|
return web.RespondJsonError(ctx, w, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := user.Create(ctx, claims, u.MasterDB, req, v.Now)
|
res, err := user.Create(ctx, claims, u.MasterDB, req, v.Now)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch err {
|
cause := errors.Cause(err)
|
||||||
|
switch cause {
|
||||||
case user.ErrForbidden:
|
case user.ErrForbidden:
|
||||||
return web.NewRequestError(err, http.StatusForbidden)
|
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusForbidden))
|
||||||
default:
|
default:
|
||||||
_, ok := err.(validator.ValidationErrors)
|
_, ok := cause.(validator.ValidationErrors)
|
||||||
if ok {
|
if ok {
|
||||||
return web.NewRequestError(err, http.StatusBadRequest)
|
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusBadRequest))
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors.Wrapf(err, "User: %+v", &req)
|
return errors.Wrapf(err, "User: %+v", &req)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if claims.Audience != "" {
|
|
||||||
uaReq := user_account.UserAccountCreateRequest{
|
|
||||||
UserID: resp.User.ID,
|
|
||||||
AccountID: resp.Account.ID,
|
|
||||||
Roles: []user_account.UserAccountRole{user_account.UserAccountRole_Admin},
|
|
||||||
//Status: Use default value
|
|
||||||
}
|
|
||||||
_, err = user_account.Create(ctx, claims, u.MasterDB, uaReq, v.Now)
|
|
||||||
if err != nil {
|
|
||||||
switch err {
|
|
||||||
case user.ErrForbidden:
|
|
||||||
return web.NewRequestError(err, http.StatusForbidden)
|
|
||||||
default:
|
|
||||||
_, ok := err.(validator.ValidationErrors)
|
|
||||||
if ok {
|
|
||||||
return web.NewRequestError(err, http.StatusBadRequest)
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors.Wrapf(err, "User account: %+v", &req)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return web.RespondJson(ctx, w, res.Response(ctx), http.StatusCreated)
|
return web.RespondJson(ctx, w, res.Response(ctx), http.StatusCreated)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -263,10 +226,9 @@ func (u *User) Create(ctx context.Context, w http.ResponseWriter, r *http.Reques
|
|||||||
// @Produce json
|
// @Produce json
|
||||||
// @Security OAuth2Password
|
// @Security OAuth2Password
|
||||||
// @Param data body user.UserUpdateRequest true "Update fields"
|
// @Param data body user.UserUpdateRequest true "Update fields"
|
||||||
// @Success 201
|
// @Success 204
|
||||||
// @Failure 400 {object} web.ErrorResponse
|
// @Failure 400 {object} web.ErrorResponse
|
||||||
// @Failure 403 {object} web.ErrorResponse
|
// @Failure 403 {object} web.ErrorResponse
|
||||||
// @Failure 404 {object} web.ErrorResponse
|
|
||||||
// @Failure 500 {object} web.ErrorResponse
|
// @Failure 500 {object} web.ErrorResponse
|
||||||
// @Router /users [patch]
|
// @Router /users [patch]
|
||||||
func (u *User) Update(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
func (u *User) Update(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||||
@ -282,28 +244,22 @@ func (u *User) Update(ctx context.Context, w http.ResponseWriter, r *http.Reques
|
|||||||
|
|
||||||
var req user.UserUpdateRequest
|
var req user.UserUpdateRequest
|
||||||
if err := web.Decode(r, &req); err != nil {
|
if err := web.Decode(r, &req); err != nil {
|
||||||
err = errors.WithStack(err)
|
if _, ok := errors.Cause(err).(*web.Error); !ok {
|
||||||
|
err = web.NewRequestError(err, http.StatusBadRequest)
|
||||||
_, ok := err.(validator.ValidationErrors)
|
|
||||||
if ok {
|
|
||||||
return web.NewRequestError(err, http.StatusBadRequest)
|
|
||||||
}
|
}
|
||||||
return err
|
return web.RespondJsonError(ctx, w, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err := user.Update(ctx, claims, u.MasterDB, req, v.Now)
|
err := user.Update(ctx, claims, u.MasterDB, req, v.Now)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch err {
|
cause := errors.Cause(err)
|
||||||
case user.ErrInvalidID:
|
switch cause {
|
||||||
return web.NewRequestError(err, http.StatusBadRequest)
|
|
||||||
case user.ErrNotFound:
|
|
||||||
return web.NewRequestError(err, http.StatusNotFound)
|
|
||||||
case user.ErrForbidden:
|
case user.ErrForbidden:
|
||||||
return web.NewRequestError(err, http.StatusForbidden)
|
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusForbidden))
|
||||||
default:
|
default:
|
||||||
_, ok := err.(validator.ValidationErrors)
|
_, ok := cause.(validator.ValidationErrors)
|
||||||
if ok {
|
if ok {
|
||||||
return web.NewRequestError(err, http.StatusBadRequest)
|
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusBadRequest))
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors.Wrapf(err, "Id: %s User: %+v", req.ID, &req)
|
return errors.Wrapf(err, "Id: %s User: %+v", req.ID, &req)
|
||||||
@ -321,10 +277,9 @@ func (u *User) Update(ctx context.Context, w http.ResponseWriter, r *http.Reques
|
|||||||
// @Produce json
|
// @Produce json
|
||||||
// @Security OAuth2Password
|
// @Security OAuth2Password
|
||||||
// @Param data body user.UserUpdatePasswordRequest true "Update fields"
|
// @Param data body user.UserUpdatePasswordRequest true "Update fields"
|
||||||
// @Success 201
|
// @Success 204
|
||||||
// @Failure 400 {object} web.ErrorResponse
|
// @Failure 400 {object} web.ErrorResponse
|
||||||
// @Failure 403 {object} web.ErrorResponse
|
// @Failure 403 {object} web.ErrorResponse
|
||||||
// @Failure 404 {object} web.ErrorResponse
|
|
||||||
// @Failure 500 {object} web.ErrorResponse
|
// @Failure 500 {object} web.ErrorResponse
|
||||||
// @Router /users/password [patch]
|
// @Router /users/password [patch]
|
||||||
func (u *User) UpdatePassword(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
func (u *User) UpdatePassword(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||||
@ -340,28 +295,24 @@ func (u *User) UpdatePassword(ctx context.Context, w http.ResponseWriter, r *htt
|
|||||||
|
|
||||||
var req user.UserUpdatePasswordRequest
|
var req user.UserUpdatePasswordRequest
|
||||||
if err := web.Decode(r, &req); err != nil {
|
if err := web.Decode(r, &req); err != nil {
|
||||||
err = errors.WithStack(err)
|
if _, ok := errors.Cause(err).(*web.Error); !ok {
|
||||||
|
err = web.NewRequestError(err, http.StatusBadRequest)
|
||||||
_, ok := err.(validator.ValidationErrors)
|
|
||||||
if ok {
|
|
||||||
return web.NewRequestError(err, http.StatusBadRequest)
|
|
||||||
}
|
}
|
||||||
return err
|
return web.RespondJsonError(ctx, w, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err := user.UpdatePassword(ctx, claims, u.MasterDB, req, v.Now)
|
err := user.UpdatePassword(ctx, claims, u.MasterDB, req, v.Now)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch err {
|
cause := errors.Cause(err)
|
||||||
case user.ErrInvalidID:
|
switch cause {
|
||||||
return web.NewRequestError(err, http.StatusBadRequest)
|
|
||||||
case user.ErrNotFound:
|
case user.ErrNotFound:
|
||||||
return web.NewRequestError(err, http.StatusNotFound)
|
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusNotFound))
|
||||||
case user.ErrForbidden:
|
case user.ErrForbidden:
|
||||||
return web.NewRequestError(err, http.StatusForbidden)
|
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusForbidden))
|
||||||
default:
|
default:
|
||||||
_, ok := err.(validator.ValidationErrors)
|
_, ok := cause.(validator.ValidationErrors)
|
||||||
if ok {
|
if ok {
|
||||||
return web.NewRequestError(err, http.StatusBadRequest)
|
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusBadRequest))
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors.Wrapf(err, "Id: %s User: %+v", req.ID, &req)
|
return errors.Wrapf(err, "Id: %s User: %+v", req.ID, &req)
|
||||||
@ -379,7 +330,7 @@ func (u *User) UpdatePassword(ctx context.Context, w http.ResponseWriter, r *htt
|
|||||||
// @Produce json
|
// @Produce json
|
||||||
// @Security OAuth2Password
|
// @Security OAuth2Password
|
||||||
// @Param data body user.UserArchiveRequest true "Update fields"
|
// @Param data body user.UserArchiveRequest true "Update fields"
|
||||||
// @Success 201
|
// @Success 204
|
||||||
// @Failure 400 {object} web.ErrorResponse
|
// @Failure 400 {object} web.ErrorResponse
|
||||||
// @Failure 403 {object} web.ErrorResponse
|
// @Failure 403 {object} web.ErrorResponse
|
||||||
// @Failure 404 {object} web.ErrorResponse
|
// @Failure 404 {object} web.ErrorResponse
|
||||||
@ -398,28 +349,24 @@ func (u *User) Archive(ctx context.Context, w http.ResponseWriter, r *http.Reque
|
|||||||
|
|
||||||
var req user.UserArchiveRequest
|
var req user.UserArchiveRequest
|
||||||
if err := web.Decode(r, &req); err != nil {
|
if err := web.Decode(r, &req); err != nil {
|
||||||
err = errors.WithStack(err)
|
if _, ok := errors.Cause(err).(*web.Error); !ok {
|
||||||
|
err = web.NewRequestError(err, http.StatusBadRequest)
|
||||||
_, ok := err.(validator.ValidationErrors)
|
|
||||||
if ok {
|
|
||||||
return web.NewRequestError(err, http.StatusBadRequest)
|
|
||||||
}
|
}
|
||||||
return err
|
return web.RespondJsonError(ctx, w, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err := user.Archive(ctx, claims, u.MasterDB, req, v.Now)
|
err := user.Archive(ctx, claims, u.MasterDB, req, v.Now)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch err {
|
cause := errors.Cause(err)
|
||||||
case user.ErrInvalidID:
|
switch cause {
|
||||||
return web.NewRequestError(err, http.StatusBadRequest)
|
|
||||||
case user.ErrNotFound:
|
case user.ErrNotFound:
|
||||||
return web.NewRequestError(err, http.StatusNotFound)
|
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusNotFound))
|
||||||
case user.ErrForbidden:
|
case user.ErrForbidden:
|
||||||
return web.NewRequestError(err, http.StatusForbidden)
|
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusForbidden))
|
||||||
default:
|
default:
|
||||||
_, ok := err.(validator.ValidationErrors)
|
_, ok := cause.(validator.ValidationErrors)
|
||||||
if ok {
|
if ok {
|
||||||
return web.NewRequestError(err, http.StatusBadRequest)
|
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusBadRequest))
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors.Wrapf(err, "Id: %s", req.ID)
|
return errors.Wrapf(err, "Id: %s", req.ID)
|
||||||
@ -437,7 +384,7 @@ func (u *User) Archive(ctx context.Context, w http.ResponseWriter, r *http.Reque
|
|||||||
// @Produce json
|
// @Produce json
|
||||||
// @Security OAuth2Password
|
// @Security OAuth2Password
|
||||||
// @Param id path string true "User ID"
|
// @Param id path string true "User ID"
|
||||||
// @Success 201
|
// @Success 204
|
||||||
// @Failure 400 {object} web.ErrorResponse
|
// @Failure 400 {object} web.ErrorResponse
|
||||||
// @Failure 403 {object} web.ErrorResponse
|
// @Failure 403 {object} web.ErrorResponse
|
||||||
// @Failure 404 {object} web.ErrorResponse
|
// @Failure 404 {object} web.ErrorResponse
|
||||||
@ -451,15 +398,14 @@ func (u *User) Delete(ctx context.Context, w http.ResponseWriter, r *http.Reques
|
|||||||
|
|
||||||
err := user.Delete(ctx, claims, u.MasterDB, params["id"])
|
err := user.Delete(ctx, claims, u.MasterDB, params["id"])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch err {
|
cause := errors.Cause(err)
|
||||||
case user.ErrInvalidID:
|
switch cause {
|
||||||
return web.NewRequestError(err, http.StatusBadRequest)
|
|
||||||
case user.ErrNotFound:
|
case user.ErrNotFound:
|
||||||
return web.NewRequestError(err, http.StatusNotFound)
|
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusNotFound))
|
||||||
case user.ErrForbidden:
|
case user.ErrForbidden:
|
||||||
return web.NewRequestError(err, http.StatusForbidden)
|
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusForbidden))
|
||||||
default:
|
default:
|
||||||
return errors.Wrapf(err, "Id: %s", params["id"])
|
return errors.Wrapf(cause, "Id: %s", params["id"])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -493,13 +439,14 @@ func (u *User) SwitchAccount(ctx context.Context, w http.ResponseWriter, r *http
|
|||||||
|
|
||||||
tkn, err := user.SwitchAccount(ctx, u.MasterDB, u.TokenGenerator, claims, params["account_id"], sessionTtl, v.Now)
|
tkn, err := user.SwitchAccount(ctx, u.MasterDB, u.TokenGenerator, claims, params["account_id"], sessionTtl, v.Now)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch err {
|
cause := errors.Cause(err)
|
||||||
|
switch cause {
|
||||||
case user.ErrAuthenticationFailure:
|
case user.ErrAuthenticationFailure:
|
||||||
return web.NewRequestError(err, http.StatusUnauthorized)
|
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusUnauthorized))
|
||||||
default:
|
default:
|
||||||
_, ok := err.(validator.ValidationErrors)
|
_, ok := cause.(validator.ValidationErrors)
|
||||||
if ok {
|
if ok {
|
||||||
return web.NewRequestError(err, http.StatusBadRequest)
|
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusBadRequest))
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors.Wrap(err, "switch account")
|
return errors.Wrap(err, "switch account")
|
||||||
@ -521,7 +468,6 @@ func (u *User) SwitchAccount(ctx context.Context, w http.ResponseWriter, r *http
|
|||||||
// @Header 200 {string} Token "qwerty"
|
// @Header 200 {string} Token "qwerty"
|
||||||
// @Failure 400 {object} web.Error
|
// @Failure 400 {object} web.Error
|
||||||
// @Failure 403 {object} web.Error
|
// @Failure 403 {object} web.Error
|
||||||
// @Failure 404 {object} web.Error
|
|
||||||
// @Router /oauth/token [post]
|
// @Router /oauth/token [post]
|
||||||
func (u *User) Token(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
func (u *User) Token(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||||
v, ok := ctx.Value(web.KeyValues).(*web.Values)
|
v, ok := ctx.Value(web.KeyValues).(*web.Values)
|
||||||
@ -532,7 +478,7 @@ func (u *User) Token(ctx context.Context, w http.ResponseWriter, r *http.Request
|
|||||||
email, pass, ok := r.BasicAuth()
|
email, pass, ok := r.BasicAuth()
|
||||||
if !ok {
|
if !ok {
|
||||||
err := errors.New("must provide email and password in Basic auth")
|
err := errors.New("must provide email and password in Basic auth")
|
||||||
return web.NewRequestError(err, http.StatusUnauthorized)
|
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusUnauthorized))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optional to include scope.
|
// Optional to include scope.
|
||||||
@ -540,9 +486,10 @@ func (u *User) Token(ctx context.Context, w http.ResponseWriter, r *http.Request
|
|||||||
|
|
||||||
tkn, err := user.Authenticate(ctx, u.MasterDB, u.TokenGenerator, email, pass, sessionTtl, v.Now, scope)
|
tkn, err := user.Authenticate(ctx, u.MasterDB, u.TokenGenerator, email, pass, sessionTtl, v.Now, scope)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch err {
|
cause := errors.Cause(err)
|
||||||
|
switch cause {
|
||||||
case user.ErrAuthenticationFailure:
|
case user.ErrAuthenticationFailure:
|
||||||
return web.NewRequestError(err, http.StatusUnauthorized)
|
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusUnauthorized))
|
||||||
default:
|
default:
|
||||||
return errors.Wrap(err, "authenticating")
|
return errors.Wrap(err, "authenticating")
|
||||||
}
|
}
|
||||||
|
368
example-project/cmd/web-api/handlers/user_account.go
Normal file
368
example-project/cmd/web-api/handlers/user_account.go
Normal file
@ -0,0 +1,368 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth"
|
||||||
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web"
|
||||||
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/user_account"
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gopkg.in/go-playground/validator.v9"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UserAccount represents the UserAccount API method handler set.
|
||||||
|
type UserAccount struct {
|
||||||
|
MasterDB *sqlx.DB
|
||||||
|
|
||||||
|
// ADD OTHER STATE LIKE THE LOGGER AND CONFIG HERE.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find godoc
|
||||||
|
// @Summary List user accounts
|
||||||
|
// @Description Find returns the existing user accounts in the system.
|
||||||
|
// @Tags user_account
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security OAuth2Password
|
||||||
|
// @Param where query string false "Filter string, example: account_id = 'c4653bf9-5978-48b7-89c5-95704aebb7e2'"
|
||||||
|
// @Param order query string false "Order columns separated by comma, example: created_at desc"
|
||||||
|
// @Param limit query integer false "Limit, example: 10"
|
||||||
|
// @Param offset query integer false "Offset, example: 20"
|
||||||
|
// @Param included-archived query boolean false "Included Archived, example: false"
|
||||||
|
// @Success 200 {array} user_account.UserAccountResponse
|
||||||
|
// @Failure 400 {object} web.ErrorResponse
|
||||||
|
// @Failure 403 {object} web.ErrorResponse
|
||||||
|
// @Failure 500 {object} web.ErrorResponse
|
||||||
|
// @Router /user_accounts [get]
|
||||||
|
func (u *UserAccount) Find(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||||
|
claims, ok := ctx.Value(auth.Key).(auth.Claims)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("claims missing from context")
|
||||||
|
}
|
||||||
|
|
||||||
|
var req user_account.UserAccountFindRequest
|
||||||
|
|
||||||
|
// Handle where query value if set.
|
||||||
|
if v := r.URL.Query().Get("where"); v != "" {
|
||||||
|
where, args, err := web.ExtractWhereArgs(v)
|
||||||
|
if err != nil {
|
||||||
|
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusBadRequest))
|
||||||
|
}
|
||||||
|
req.Where = &where
|
||||||
|
req.Args = args
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle order query value if set.
|
||||||
|
if v := r.URL.Query().Get("order"); v != "" {
|
||||||
|
for _, o := range strings.Split(v, ",") {
|
||||||
|
o = strings.TrimSpace(o)
|
||||||
|
if o != "" {
|
||||||
|
req.Order = append(req.Order, o)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle limit query value if set.
|
||||||
|
if v := r.URL.Query().Get("limit"); v != "" {
|
||||||
|
l, err := strconv.Atoi(v)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.WithMessagef(err, "unable to parse %s as int for limit param", v)
|
||||||
|
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusBadRequest))
|
||||||
|
}
|
||||||
|
ul := uint(l)
|
||||||
|
req.Limit = &ul
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle offset query value if set.
|
||||||
|
if v := r.URL.Query().Get("offset"); v != "" {
|
||||||
|
l, err := strconv.Atoi(v)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.WithMessagef(err, "unable to parse %s as int for offset param", v)
|
||||||
|
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusBadRequest))
|
||||||
|
}
|
||||||
|
ul := uint(l)
|
||||||
|
req.Limit = &ul
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle order query value if set.
|
||||||
|
if v := r.URL.Query().Get("included-archived"); v != "" {
|
||||||
|
b, err := strconv.ParseBool(v)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.WithMessagef(err, "unable to parse %s as boolean for included-archived param", v)
|
||||||
|
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusBadRequest))
|
||||||
|
}
|
||||||
|
req.IncludedArchived = b
|
||||||
|
}
|
||||||
|
|
||||||
|
//if err := web.Decode(r, &req); err != nil {
|
||||||
|
// if _, ok := errors.Cause(err).(*web.Error); !ok {
|
||||||
|
// err = web.NewRequestError(err, http.StatusBadRequest)
|
||||||
|
// }
|
||||||
|
// return web.RespondJsonError(ctx, w, err)
|
||||||
|
//}
|
||||||
|
|
||||||
|
res, err := user_account.Find(ctx, claims, u.MasterDB, req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp []*user_account.UserAccountResponse
|
||||||
|
for _, m := range res {
|
||||||
|
resp = append(resp, m.Response(ctx))
|
||||||
|
}
|
||||||
|
|
||||||
|
return web.RespondJson(ctx, w, resp, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read godoc
|
||||||
|
// @Summary Get user account by ID
|
||||||
|
// @Description Read returns the specified user account from the system.
|
||||||
|
// @Tags user_account
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security OAuth2Password
|
||||||
|
// @Param id path string true "UserAccount ID"
|
||||||
|
// @Success 200 {object} user_account.UserAccountResponse
|
||||||
|
// @Failure 400 {object} web.ErrorResponse
|
||||||
|
// @Failure 404 {object} web.ErrorResponse
|
||||||
|
// @Failure 500 {object} web.ErrorResponse
|
||||||
|
// @Router /user_accounts/{id} [get]
|
||||||
|
func (u *UserAccount) Read(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||||
|
claims, ok := ctx.Value(auth.Key).(auth.Claims)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("claims missing from context")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle included-archived query value if set.
|
||||||
|
var includeArchived bool
|
||||||
|
if v := r.URL.Query().Get("included-archived"); v != "" {
|
||||||
|
b, err := strconv.ParseBool(v)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.WithMessagef(err, "unable to parse %s as boolean for included-archived param", v)
|
||||||
|
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusBadRequest))
|
||||||
|
}
|
||||||
|
includeArchived = b
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := user_account.Read(ctx, claims, u.MasterDB, params["id"], includeArchived)
|
||||||
|
if err != nil {
|
||||||
|
cause := errors.Cause(err)
|
||||||
|
switch cause {
|
||||||
|
case user_account.ErrNotFound:
|
||||||
|
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusNotFound))
|
||||||
|
default:
|
||||||
|
return errors.Wrapf(err, "ID: %s", params["id"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return web.RespondJson(ctx, w, res.Response(ctx), http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create godoc
|
||||||
|
// @Summary Create new user account.
|
||||||
|
// @Description Create inserts a new user account into the system.
|
||||||
|
// @Tags user_account
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security OAuth2Password
|
||||||
|
// @Param data body user_account.UserAccountCreateRequest true "User Account details"
|
||||||
|
// @Success 201 {object} user_account.UserAccountResponse
|
||||||
|
// @Failure 400 {object} web.ErrorResponse
|
||||||
|
// @Failure 403 {object} web.ErrorResponse
|
||||||
|
// @Failure 404 {object} web.ErrorResponse
|
||||||
|
// @Failure 500 {object} web.ErrorResponse
|
||||||
|
// @Router /user_accounts [post]
|
||||||
|
func (u *UserAccount) Create(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||||
|
v, ok := ctx.Value(web.KeyValues).(*web.Values)
|
||||||
|
if !ok {
|
||||||
|
return web.NewShutdownError("web value missing from context")
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, ok := ctx.Value(auth.Key).(auth.Claims)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("claims missing from context")
|
||||||
|
}
|
||||||
|
|
||||||
|
var req user_account.UserAccountCreateRequest
|
||||||
|
if err := web.Decode(r, &req); err != nil {
|
||||||
|
if _, ok := errors.Cause(err).(*web.Error); !ok {
|
||||||
|
err = web.NewRequestError(err, http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
return web.RespondJsonError(ctx, w, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := user_account.Create(ctx, claims, u.MasterDB, req, v.Now)
|
||||||
|
if err != nil {
|
||||||
|
cause := errors.Cause(err)
|
||||||
|
switch cause {
|
||||||
|
case user_account.ErrForbidden:
|
||||||
|
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusForbidden))
|
||||||
|
default:
|
||||||
|
_, ok := cause.(validator.ValidationErrors)
|
||||||
|
if ok {
|
||||||
|
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusBadRequest))
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.Wrapf(err, "User Account: %+v", &req)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return web.RespondJson(ctx, w, res.Response(ctx), http.StatusCreated)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read godoc
|
||||||
|
// @Summary Update user account by user ID and account ID
|
||||||
|
// @Description Update updates the specified user account in the system.
|
||||||
|
// @Tags user
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security OAuth2Password
|
||||||
|
// @Param data body user_account.UserAccountUpdateRequest true "Update fields"
|
||||||
|
// @Success 204
|
||||||
|
// @Failure 400 {object} web.ErrorResponse
|
||||||
|
// @Failure 403 {object} web.ErrorResponse
|
||||||
|
// @Failure 500 {object} web.ErrorResponse
|
||||||
|
// @Router /user_accounts [patch]
|
||||||
|
func (u *UserAccount) Update(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||||
|
v, ok := ctx.Value(web.KeyValues).(*web.Values)
|
||||||
|
if !ok {
|
||||||
|
return web.NewShutdownError("web value missing from context")
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, ok := ctx.Value(auth.Key).(auth.Claims)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("claims missing from context")
|
||||||
|
}
|
||||||
|
|
||||||
|
var req user_account.UserAccountUpdateRequest
|
||||||
|
if err := web.Decode(r, &req); err != nil {
|
||||||
|
if _, ok := errors.Cause(err).(*web.Error); !ok {
|
||||||
|
err = web.NewRequestError(err, http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
return web.RespondJsonError(ctx, w, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := user_account.Update(ctx, claims, u.MasterDB, req, v.Now)
|
||||||
|
if err != nil {
|
||||||
|
cause := errors.Cause(err)
|
||||||
|
switch cause {
|
||||||
|
case user_account.ErrForbidden:
|
||||||
|
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusForbidden))
|
||||||
|
default:
|
||||||
|
_, ok := cause.(validator.ValidationErrors)
|
||||||
|
if ok {
|
||||||
|
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusBadRequest))
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.Wrapf(err, "UserID: %s AccountID: %s User Account: %+v", req.UserID, req.AccountID, &req)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return web.RespondJson(ctx, w, nil, http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read godoc
|
||||||
|
// @Summary Archive user account by user ID and account ID
|
||||||
|
// @Description Archive soft-deletes the specified user account from the system.
|
||||||
|
// @Tags user
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security OAuth2Password
|
||||||
|
// @Param data body user_account.UserAccountArchiveRequest true "Update fields"
|
||||||
|
// @Success 204
|
||||||
|
// @Failure 400 {object} web.ErrorResponse
|
||||||
|
// @Failure 403 {object} web.ErrorResponse
|
||||||
|
// @Failure 404 {object} web.ErrorResponse
|
||||||
|
// @Failure 500 {object} web.ErrorResponse
|
||||||
|
// @Router /user_accounts/archive [patch]
|
||||||
|
func (u *UserAccount) Archive(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||||
|
v, ok := ctx.Value(web.KeyValues).(*web.Values)
|
||||||
|
if !ok {
|
||||||
|
return web.NewShutdownError("web value missing from context")
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, ok := ctx.Value(auth.Key).(auth.Claims)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("claims missing from context")
|
||||||
|
}
|
||||||
|
|
||||||
|
var req user_account.UserAccountArchiveRequest
|
||||||
|
if err := web.Decode(r, &req); err != nil {
|
||||||
|
if _, ok := errors.Cause(err).(*web.Error); !ok {
|
||||||
|
err = web.NewRequestError(err, http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
return web.RespondJsonError(ctx, w, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := user_account.Archive(ctx, claims, u.MasterDB, req, v.Now)
|
||||||
|
if err != nil {
|
||||||
|
cause := errors.Cause(err)
|
||||||
|
switch cause {
|
||||||
|
case user_account.ErrNotFound:
|
||||||
|
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusNotFound))
|
||||||
|
case user_account.ErrForbidden:
|
||||||
|
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusForbidden))
|
||||||
|
default:
|
||||||
|
_, ok := cause.(validator.ValidationErrors)
|
||||||
|
if ok {
|
||||||
|
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusBadRequest))
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.Wrapf(err, "UserID: %s AccountID: %s User Account: %+v", req.UserID, req.AccountID, &req)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return web.RespondJson(ctx, w, nil, http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete godoc
|
||||||
|
// @Summary Delete user account by user ID and account ID
|
||||||
|
// @Description Delete removes the specified user account from the system.
|
||||||
|
// @Tags user
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security OAuth2Password
|
||||||
|
// @Param id path string true "UserAccount ID"
|
||||||
|
// @Success 204
|
||||||
|
// @Failure 400 {object} web.ErrorResponse
|
||||||
|
// @Failure 403 {object} web.ErrorResponse
|
||||||
|
// @Failure 404 {object} web.ErrorResponse
|
||||||
|
// @Failure 500 {object} web.ErrorResponse
|
||||||
|
// @Router /user_accounts [delete]
|
||||||
|
func (u *UserAccount) Delete(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||||
|
claims, ok := ctx.Value(auth.Key).(auth.Claims)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("claims missing from context")
|
||||||
|
}
|
||||||
|
|
||||||
|
var req user_account.UserAccountDeleteRequest
|
||||||
|
if err := web.Decode(r, &req); err != nil {
|
||||||
|
if _, ok := errors.Cause(err).(*web.Error); !ok {
|
||||||
|
err = web.NewRequestError(err, http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
return web.RespondJsonError(ctx, w, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := user_account.Delete(ctx, claims, u.MasterDB, req)
|
||||||
|
if err != nil {
|
||||||
|
cause := errors.Cause(err)
|
||||||
|
switch cause {
|
||||||
|
case user_account.ErrNotFound:
|
||||||
|
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusNotFound))
|
||||||
|
case user_account.ErrForbidden:
|
||||||
|
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusForbidden))
|
||||||
|
default:
|
||||||
|
_, ok := cause.(validator.ValidationErrors)
|
||||||
|
if ok {
|
||||||
|
return web.RespondJsonError(ctx, w, web.NewRequestError(err, http.StatusBadRequest))
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.Wrapf(err, "UserID: %s AccountID: %s User Account: %+v", req.UserID, req.AccountID, &req)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return web.RespondJson(ctx, w, nil, http.StatusNoContent)
|
||||||
|
}
|
339
example-project/cmd/web-api/tests/account_test.go
Normal file
339
example-project/cmd/web-api/tests/account_test.go
Normal file
@ -0,0 +1,339 @@
|
|||||||
|
package tests
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/mid"
|
||||||
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/account"
|
||||||
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth"
|
||||||
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/tests"
|
||||||
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web"
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/pborman/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
func mockAccount() *account.Account {
|
||||||
|
req := account.AccountCreateRequest{
|
||||||
|
Name: uuid.NewRandom().String(),
|
||||||
|
Address1: "103 East Main St",
|
||||||
|
Address2: "Unit 546",
|
||||||
|
City: "Valdez",
|
||||||
|
Region: "AK",
|
||||||
|
Country: "USA",
|
||||||
|
Zipcode: "99686",
|
||||||
|
}
|
||||||
|
|
||||||
|
a, err := account.Create(tests.Context(), auth.Claims{}, test.MasterDB, req, time.Now().UTC().AddDate(-1, -1, -1))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestAccount is the entry point for the account endpoints.
|
||||||
|
func TestAccount(t *testing.T) {
|
||||||
|
defer tests.Recover(t)
|
||||||
|
|
||||||
|
t.Run("getAccount", getAccount)
|
||||||
|
t.Run("patchAccount", patchAccount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getAccount validates get account by ID endpoint.
|
||||||
|
func getAccount(t *testing.T) {
|
||||||
|
|
||||||
|
var rtests []requestTest
|
||||||
|
|
||||||
|
forbiddenAccount := mockAccount()
|
||||||
|
|
||||||
|
// Both roles should be able to read the account.
|
||||||
|
for rn, tr := range roleTests {
|
||||||
|
acc := tr.SignupResult.Account
|
||||||
|
|
||||||
|
// Test 200.
|
||||||
|
rtests = append(rtests, requestTest{
|
||||||
|
fmt.Sprintf("Role %s 200", rn),
|
||||||
|
http.MethodGet,
|
||||||
|
fmt.Sprintf("/v1/accounts/%s", acc.ID),
|
||||||
|
nil,
|
||||||
|
tr.Token,
|
||||||
|
tr.Claims,
|
||||||
|
http.StatusOK,
|
||||||
|
nil,
|
||||||
|
func(treq requestTest, body []byte) bool {
|
||||||
|
var actual account.AccountResponse
|
||||||
|
if err := json.Unmarshal(body, &actual); err != nil {
|
||||||
|
t.Logf("\t\tGot error : %+v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add claims to the context so they can be retrieved later.
|
||||||
|
ctx := context.WithValue(tests.Context(), auth.Key, tr.Claims)
|
||||||
|
|
||||||
|
expectedMap := map[string]interface{}{
|
||||||
|
"updated_at": web.NewTimeResponse(ctx, acc.UpdatedAt),
|
||||||
|
"id": acc.ID,
|
||||||
|
"address2": acc.Address2,
|
||||||
|
"region": acc.Region,
|
||||||
|
"zipcode": acc.Zipcode,
|
||||||
|
"timezone": acc.Timezone,
|
||||||
|
"created_at": web.NewTimeResponse(ctx, acc.CreatedAt),
|
||||||
|
"country": acc.Country,
|
||||||
|
"billing_user_id": &acc.BillingUserID,
|
||||||
|
"name": acc.Name,
|
||||||
|
"address1": acc.Address1,
|
||||||
|
"city": acc.City,
|
||||||
|
"status": map[string]interface{}{
|
||||||
|
"value": "active",
|
||||||
|
"title": "Active",
|
||||||
|
"options": []map[string]interface{}{{"selected": false, "title": "[Active Pending Disabled]", "value": "[active pending disabled]"}},
|
||||||
|
},
|
||||||
|
"signup_user_id": &acc.SignupUserID,
|
||||||
|
}
|
||||||
|
expectedJson, err := json.Marshal(expectedMap)
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("\t\tGot error : %+v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var expected account.AccountResponse
|
||||||
|
if err := json.Unmarshal([]byte(expectedJson), &expected); err != nil {
|
||||||
|
t.Logf("\t\tGot error : %+v", err)
|
||||||
|
printResultMap(ctx, body)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff := cmp.Diff(actual, expected); diff != "" {
|
||||||
|
actualJSON, err := json.MarshalIndent(actual, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("\t\tGot error : %+v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
t.Logf("\t\tGot : %s\n", actualJSON)
|
||||||
|
|
||||||
|
expectedJSON, err := json.MarshalIndent(expected, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("\t\tGot error : %+v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
t.Logf("\t\tExpected : %s\n", expectedJSON)
|
||||||
|
|
||||||
|
t.Logf("\t\tDiff : %s\n", diff)
|
||||||
|
|
||||||
|
if len(expectedMap) == 0 {
|
||||||
|
printResultMap(ctx, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test 404.
|
||||||
|
invalidID := uuid.NewRandom().String()
|
||||||
|
rtests = append(rtests, requestTest{
|
||||||
|
fmt.Sprintf("Role %s 404 w/invalid ID", rn),
|
||||||
|
http.MethodGet,
|
||||||
|
fmt.Sprintf("/v1/accounts/%s", invalidID),
|
||||||
|
nil,
|
||||||
|
tr.Token,
|
||||||
|
tr.Claims,
|
||||||
|
http.StatusNotFound,
|
||||||
|
web.ErrorResponse{
|
||||||
|
Error: fmt.Sprintf("account %s not found: Entity not found", invalidID),
|
||||||
|
},
|
||||||
|
func(treq requestTest, body []byte) bool {
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test 404 - Account exists but not allowed.
|
||||||
|
rtests = append(rtests, requestTest{
|
||||||
|
fmt.Sprintf("Role %s 404 w/random account ID", rn),
|
||||||
|
http.MethodGet,
|
||||||
|
fmt.Sprintf("/v1/accounts/%s", forbiddenAccount.ID),
|
||||||
|
nil,
|
||||||
|
tr.Token,
|
||||||
|
tr.Claims,
|
||||||
|
http.StatusNotFound,
|
||||||
|
web.ErrorResponse{
|
||||||
|
Error: fmt.Sprintf("account %s not found: Entity not found", forbiddenAccount.ID),
|
||||||
|
},
|
||||||
|
func(treq requestTest, body []byte) bool {
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
runRequestTests(t, rtests)
|
||||||
|
}
|
||||||
|
|
||||||
|
// patchAccount validates update account by ID endpoint.
|
||||||
|
func patchAccount(t *testing.T) {
|
||||||
|
|
||||||
|
var rtests []requestTest
|
||||||
|
|
||||||
|
// Test update an account
|
||||||
|
// Admin role: 204
|
||||||
|
// User role 403
|
||||||
|
for rn, tr := range roleTests {
|
||||||
|
var expectedStatus int
|
||||||
|
var expectedErr interface{}
|
||||||
|
|
||||||
|
// Test 204.
|
||||||
|
if rn == auth.RoleAdmin {
|
||||||
|
expectedStatus = http.StatusNoContent
|
||||||
|
} else {
|
||||||
|
expectedStatus = http.StatusForbidden
|
||||||
|
expectedErr = web.ErrorResponse{
|
||||||
|
Error: mid.ErrForbidden.Error(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newName := rn + uuid.NewRandom().String() + strconv.Itoa(len(rtests))
|
||||||
|
rtests = append(rtests, requestTest{
|
||||||
|
fmt.Sprintf("Role %s %d", rn, expectedStatus),
|
||||||
|
http.MethodPatch,
|
||||||
|
"/v1/accounts",
|
||||||
|
account.AccountUpdateRequest{
|
||||||
|
ID: tr.SignupResult.Account.ID,
|
||||||
|
Name: &newName,
|
||||||
|
},
|
||||||
|
tr.Token,
|
||||||
|
tr.Claims,
|
||||||
|
expectedStatus,
|
||||||
|
expectedErr,
|
||||||
|
func(treq requestTest, body []byte) bool {
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test update an account with invalid data.
|
||||||
|
// Admin role: 400
|
||||||
|
// User role 400
|
||||||
|
for rn, tr := range roleTests {
|
||||||
|
var expectedStatus int
|
||||||
|
var expectedErr interface{}
|
||||||
|
|
||||||
|
if rn == auth.RoleAdmin {
|
||||||
|
expectedStatus = http.StatusBadRequest
|
||||||
|
expectedErr = web.ErrorResponse{
|
||||||
|
Error: "field validation error",
|
||||||
|
Fields: []web.FieldError{
|
||||||
|
{Field: "status", Error: "Key: 'AccountUpdateRequest.status' Error:Field validation for 'status' failed on the 'oneof' tag"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
expectedStatus = http.StatusForbidden
|
||||||
|
expectedErr = web.ErrorResponse{
|
||||||
|
Error: mid.ErrForbidden.Error(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
invalidStatus := account.AccountStatus("invalid status")
|
||||||
|
rtests = append(rtests, requestTest{
|
||||||
|
fmt.Sprintf("Role %s %d w/invalid data", rn, expectedStatus),
|
||||||
|
http.MethodPatch,
|
||||||
|
"/v1/accounts",
|
||||||
|
account.AccountUpdateRequest{
|
||||||
|
ID: tr.SignupResult.User.ID,
|
||||||
|
Status: &invalidStatus,
|
||||||
|
},
|
||||||
|
tr.Token,
|
||||||
|
tr.Claims,
|
||||||
|
expectedStatus,
|
||||||
|
expectedErr,
|
||||||
|
func(treq requestTest, body []byte) bool {
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test update an account for with an invalid ID.
|
||||||
|
// Admin role: 403
|
||||||
|
// User role 403
|
||||||
|
for rn, tr := range roleTests {
|
||||||
|
var expectedStatus int
|
||||||
|
var expectedErr interface{}
|
||||||
|
|
||||||
|
// Test 403.
|
||||||
|
if rn == auth.RoleAdmin {
|
||||||
|
expectedStatus = http.StatusForbidden
|
||||||
|
expectedErr = web.ErrorResponse{
|
||||||
|
Error: account.ErrForbidden.Error(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
expectedStatus = http.StatusForbidden
|
||||||
|
expectedErr = web.ErrorResponse{
|
||||||
|
Error: mid.ErrForbidden.Error(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newName := rn + uuid.NewRandom().String() + strconv.Itoa(len(rtests))
|
||||||
|
invalidID := uuid.NewRandom().String()
|
||||||
|
rtests = append(rtests, requestTest{
|
||||||
|
fmt.Sprintf("Role %s %d w/invalid ID", rn, expectedStatus),
|
||||||
|
http.MethodPatch,
|
||||||
|
"/v1/accounts",
|
||||||
|
account.AccountUpdateRequest{
|
||||||
|
ID: invalidID,
|
||||||
|
Name: &newName,
|
||||||
|
},
|
||||||
|
tr.Token,
|
||||||
|
tr.Claims,
|
||||||
|
expectedStatus,
|
||||||
|
expectedErr,
|
||||||
|
func(treq requestTest, body []byte) bool {
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test update an account for with random account ID.
|
||||||
|
// Admin role: 403
|
||||||
|
// User role 403
|
||||||
|
forbiddenAccount := mockAccount()
|
||||||
|
for rn, tr := range roleTests {
|
||||||
|
var expectedStatus int
|
||||||
|
var expectedErr interface{}
|
||||||
|
|
||||||
|
// Test 403.
|
||||||
|
if rn == auth.RoleAdmin {
|
||||||
|
expectedStatus = http.StatusForbidden
|
||||||
|
expectedErr = web.ErrorResponse{
|
||||||
|
Error: account.ErrForbidden.Error(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
expectedStatus = http.StatusForbidden
|
||||||
|
expectedErr = web.ErrorResponse{
|
||||||
|
Error: mid.ErrForbidden.Error(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newName := rn+uuid.NewRandom().String()+strconv.Itoa(len(rtests))
|
||||||
|
rtests = append(rtests, requestTest{
|
||||||
|
fmt.Sprintf("Role %s %d w/random account ID", rn, expectedStatus),
|
||||||
|
http.MethodPatch,
|
||||||
|
"/v1/accounts",
|
||||||
|
account.AccountUpdateRequest{
|
||||||
|
ID: forbiddenAccount.ID,
|
||||||
|
Name: &newName,
|
||||||
|
},
|
||||||
|
tr.Token,
|
||||||
|
tr.Claims,
|
||||||
|
expectedStatus,
|
||||||
|
expectedErr,
|
||||||
|
func(treq requestTest, body []byte) bool {
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
runRequestTests(t, rtests)
|
||||||
|
}
|
185
example-project/cmd/web-api/tests/signup_test.go
Normal file
185
example-project/cmd/web-api/tests/signup_test.go
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
package tests
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth"
|
||||||
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/tests"
|
||||||
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web"
|
||||||
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/signup"
|
||||||
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/user"
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/pborman/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
func mockSignupRequest() signup.SignupRequest {
|
||||||
|
return signup.SignupRequest{
|
||||||
|
Account: signup.SignupAccount{
|
||||||
|
Name: uuid.NewRandom().String(),
|
||||||
|
Address1: "103 East Main St",
|
||||||
|
Address2: "Unit 546",
|
||||||
|
City: "Valdez",
|
||||||
|
Region: "AK",
|
||||||
|
Country: "USA",
|
||||||
|
Zipcode: "99686",
|
||||||
|
},
|
||||||
|
User: signup.SignupUser{
|
||||||
|
Name: "Lee Brown",
|
||||||
|
Email: uuid.NewRandom().String() + "@geeksinthewoods.com",
|
||||||
|
Password: "akTechFr0n!ier",
|
||||||
|
PasswordConfirm: "akTechFr0n!ier",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSignup is the entry point for the signup
|
||||||
|
func TestSignup(t *testing.T) {
|
||||||
|
defer tests.Recover(t)
|
||||||
|
|
||||||
|
t.Run("postSigup", postSigup)
|
||||||
|
}
|
||||||
|
|
||||||
|
// postSigup validates the signup endpoint.
|
||||||
|
func postSigup(t *testing.T) {
|
||||||
|
|
||||||
|
var rtests []requestTest
|
||||||
|
|
||||||
|
// Test 201.
|
||||||
|
// Signup does not require auth, so empty token and claims should result in success.
|
||||||
|
req1 := mockSignupRequest()
|
||||||
|
rtests = append(rtests, requestTest{
|
||||||
|
"No Authorization Valid",
|
||||||
|
http.MethodPost,
|
||||||
|
"/v1/signup",
|
||||||
|
req1,
|
||||||
|
user.Token{},
|
||||||
|
auth.Claims{},
|
||||||
|
http.StatusCreated,
|
||||||
|
nil,
|
||||||
|
func(treq requestTest, body []byte) bool {
|
||||||
|
var actual signup.SignupResponse
|
||||||
|
if err := json.Unmarshal(body, &actual); err != nil {
|
||||||
|
t.Logf("\t\tGot error : %+v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := tests.Context()
|
||||||
|
|
||||||
|
req := treq.request.(signup.SignupRequest )
|
||||||
|
|
||||||
|
expectedMap := map[string]interface{}{
|
||||||
|
"user": map[string]interface{}{
|
||||||
|
"id": actual.User.ID,
|
||||||
|
"name": req.User.Name,
|
||||||
|
"email": req.User.Email,
|
||||||
|
"timezone": actual.User.Timezone,
|
||||||
|
"created_at": web.NewTimeResponse(ctx, actual.User.CreatedAt.Value),
|
||||||
|
"updated_at": web.NewTimeResponse(ctx, actual.User.UpdatedAt.Value),
|
||||||
|
},
|
||||||
|
"account": map[string]interface{}{
|
||||||
|
"updated_at": web.NewTimeResponse(ctx, actual.Account.UpdatedAt.Value),
|
||||||
|
"id": actual.Account.ID,
|
||||||
|
"address2": req.Account.Address2,
|
||||||
|
"region": req.Account.Region,
|
||||||
|
"zipcode": req.Account.Zipcode,
|
||||||
|
"timezone": actual.Account.Timezone,
|
||||||
|
"created_at": web.NewTimeResponse(ctx, actual.Account.CreatedAt.Value),
|
||||||
|
"country": req.Account.Country,
|
||||||
|
"billing_user_id": &actual.Account.BillingUserID,
|
||||||
|
"name": req.Account.Name,
|
||||||
|
"address1": req.Account.Address1,
|
||||||
|
"city": req.Account.City,
|
||||||
|
"status": map[string]interface{}{
|
||||||
|
"value": "active",
|
||||||
|
"title": "Active",
|
||||||
|
"options": []map[string]interface{}{{"selected":false,"title":"[Active Pending Disabled]","value":"[active pending disabled]"}},
|
||||||
|
},
|
||||||
|
"signup_user_id": &actual.Account.SignupUserID,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
expectedJson, err := json.Marshal(expectedMap)
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("\t\tGot error : %+v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var expected signup.SignupResponse
|
||||||
|
if err := json.Unmarshal([]byte(expectedJson), &expected); err != nil {
|
||||||
|
t.Logf("\t\tGot error : %+v", err)
|
||||||
|
printResultMap(ctx, body)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff := cmp.Diff(actual, expected); diff != "" {
|
||||||
|
actualJSON, err := json.MarshalIndent(actual, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("\t\tGot error : %+v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
t.Logf("\t\tGot : %s\n", actualJSON)
|
||||||
|
|
||||||
|
expectedJSON, err := json.MarshalIndent(expected, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("\t\tGot error : %+v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
t.Logf("\t\tExpected : %s\n", expectedJSON)
|
||||||
|
|
||||||
|
t.Logf("\t\tDiff : %s\n", diff)
|
||||||
|
|
||||||
|
if len(expectedMap) == 0 {
|
||||||
|
printResultMap(ctx, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test 404 w/empty request.
|
||||||
|
rtests = append(rtests, requestTest{
|
||||||
|
"Empty request",
|
||||||
|
http.MethodPost,
|
||||||
|
"/v1/signup",
|
||||||
|
nil,
|
||||||
|
user.Token{},
|
||||||
|
auth.Claims{},
|
||||||
|
http.StatusBadRequest,
|
||||||
|
web.ErrorResponse{
|
||||||
|
Error: "decode request body failed: EOF",
|
||||||
|
},
|
||||||
|
func(req requestTest, body []byte) bool {
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test 404 w/validation errors.
|
||||||
|
invalidReq := mockSignupRequest()
|
||||||
|
invalidReq.User.Email = ""
|
||||||
|
invalidReq.Account.Name = ""
|
||||||
|
rtests = append(rtests, requestTest{
|
||||||
|
"Invalid request",
|
||||||
|
http.MethodPost,
|
||||||
|
"/v1/signup",
|
||||||
|
invalidReq,
|
||||||
|
user.Token{},
|
||||||
|
auth.Claims{},
|
||||||
|
http.StatusBadRequest,
|
||||||
|
web.ErrorResponse{
|
||||||
|
Error: "field validation error",
|
||||||
|
Fields: []web.FieldError{
|
||||||
|
{Field: "name", Error: "Key: 'SignupRequest.account.name' Error:Field validation for 'name' failed on the 'required' tag"},
|
||||||
|
{Field: "email", Error: "Key: 'SignupRequest.user.email' Error:Field validation for 'email' failed on the 'required' tag"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
func(req requestTest, body []byte) bool {
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
runRequestTests(t, rtests)
|
||||||
|
}
|
@ -1,14 +1,25 @@
|
|||||||
package tests
|
package tests
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/account"
|
"bytes"
|
||||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/signup"
|
"context"
|
||||||
"github.com/pborman/uuid"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web"
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/account"
|
||||||
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/signup"
|
||||||
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/user_account"
|
||||||
|
"github.com/iancoleman/strcase"
|
||||||
|
"github.com/pborman/uuid"
|
||||||
"geeks-accelerator/oss/saas-starter-kit/example-project/cmd/web-api/handlers"
|
"geeks-accelerator/oss/saas-starter-kit/example-project/cmd/web-api/handlers"
|
||||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth"
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth"
|
||||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/tests"
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/tests"
|
||||||
@ -23,11 +34,23 @@ type roleTest struct {
|
|||||||
Token user.Token
|
Token user.Token
|
||||||
Claims auth.Claims
|
Claims auth.Claims
|
||||||
SignupRequest *signup.SignupRequest
|
SignupRequest *signup.SignupRequest
|
||||||
SignupResponse *signup.SignupResponse
|
SignupResult *signup.SignupResult
|
||||||
User *user.User
|
User *user.User
|
||||||
Account *account.Account
|
Account *account.Account
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type requestTest struct {
|
||||||
|
name string
|
||||||
|
method string
|
||||||
|
url string
|
||||||
|
request interface{}
|
||||||
|
token user.Token
|
||||||
|
claims auth.Claims
|
||||||
|
statusCode int
|
||||||
|
error interface{}
|
||||||
|
expected func(req requestTest, result []byte) bool
|
||||||
|
}
|
||||||
|
|
||||||
var roleTests map[string]roleTest
|
var roleTests map[string]roleTest
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@ -92,7 +115,7 @@ func testMain(m *testing.M) int {
|
|||||||
Token: adminTkn,
|
Token: adminTkn,
|
||||||
Claims: adminClaims,
|
Claims: adminClaims,
|
||||||
SignupRequest: &signupReq,
|
SignupRequest: &signupReq,
|
||||||
SignupResponse: signup,
|
SignupResult: signup,
|
||||||
User: signup.User,
|
User: signup.User,
|
||||||
Account: signup.Account,
|
Account: signup.Account,
|
||||||
}
|
}
|
||||||
@ -109,6 +132,16 @@ func testMain(m *testing.M) int {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_, err = user_account.Create(tests.Context(), adminClaims, test.MasterDB, user_account.UserAccountCreateRequest{
|
||||||
|
UserID: usr.ID,
|
||||||
|
AccountID: signup.Account.ID,
|
||||||
|
Roles: []user_account.UserAccountRole{user_account.UserAccountRole_User},
|
||||||
|
// Status: use default value
|
||||||
|
}, now)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
userTkn, err := user.Authenticate(tests.Context(), test.MasterDB, authenticator, usr.Email, userReq.Password, expires, now)
|
userTkn, err := user.Authenticate(tests.Context(), test.MasterDB, authenticator, usr.Email, userReq.Password, expires, now)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@ -123,10 +156,130 @@ func testMain(m *testing.M) int {
|
|||||||
Token: userTkn,
|
Token: userTkn,
|
||||||
Claims: userClaims,
|
Claims: userClaims,
|
||||||
SignupRequest: &signupReq,
|
SignupRequest: &signupReq,
|
||||||
SignupResponse: signup,
|
SignupResult: signup,
|
||||||
Account: signup.Account,
|
Account: signup.Account,
|
||||||
User: usr,
|
User: usr,
|
||||||
}
|
}
|
||||||
|
|
||||||
return m.Run()
|
return m.Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// runRequestTests helper function for testing endpoints.
|
||||||
|
func runRequestTests(t *testing.T, rtests []requestTest ) {
|
||||||
|
|
||||||
|
for i, tt := range rtests {
|
||||||
|
t.Logf("\tTest: %d\tWhen running test: %s", i, tt.name)
|
||||||
|
{
|
||||||
|
var req []byte
|
||||||
|
var rr io.Reader
|
||||||
|
if tt.request != nil {
|
||||||
|
var ok bool
|
||||||
|
req, ok = tt.request.([]byte)
|
||||||
|
if !ok {
|
||||||
|
var err error
|
||||||
|
req, err = json.Marshal(tt.request)
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("\t\tGot err : %+v", err)
|
||||||
|
t.Fatalf("\t%s\tEncode request failed.", tests.Failed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rr = bytes.NewReader(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
r := httptest.NewRequest(tt.method, tt.url , rr)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
r.Header.Set("Content-Type", web.MIMEApplicationJSONCharsetUTF8)
|
||||||
|
if tt.token.AccessToken != "" {
|
||||||
|
r.Header.Set("Authorization", tt.token.AuthorizationHeader())
|
||||||
|
}
|
||||||
|
|
||||||
|
a.ServeHTTP(w, r)
|
||||||
|
|
||||||
|
if w.Code != tt.statusCode {
|
||||||
|
t.Logf("\t\tRequest : %s\n", string(req))
|
||||||
|
t.Logf("\t\tBody : %s\n", w.Body.String())
|
||||||
|
t.Fatalf("\t%s\tShould receive a status code of %d for the response : %v", tests.Failed, tt.statusCode, w.Code)
|
||||||
|
}
|
||||||
|
t.Logf("\t%s\tReceived valid status code of %d.", tests.Success, w.Code)
|
||||||
|
|
||||||
|
if tt.error != nil {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
var actual web.ErrorResponse
|
||||||
|
if err := json.Unmarshal(w.Body.Bytes(), &actual); err != nil {
|
||||||
|
t.Logf("\t\tBody : %s\n", w.Body.String())
|
||||||
|
t.Logf("\t\tGot error : %+v", err)
|
||||||
|
t.Fatalf("\t%s\tShould get the expected error.", tests.Failed)
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff := cmp.Diff(actual, tt.error); diff != "" {
|
||||||
|
t.Logf("\t\tDiff : %s\n", diff)
|
||||||
|
t.Fatalf("\t%s\tShould get the expected error.", tests.Failed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok := tt.expected(tt, w.Body.Bytes()); !ok {
|
||||||
|
t.Fatalf("\t%s\tShould get the expected result.", tests.Failed)
|
||||||
|
}
|
||||||
|
t.Logf("\t%s\tReceived expected result.", tests.Success)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func printResultMap(ctx context.Context, result []byte) {
|
||||||
|
var m map[string]interface{}
|
||||||
|
if err := json.Unmarshal(result, &m); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(`map[string]interface{}{`)
|
||||||
|
printResultMapKeys(ctx, m, 1)
|
||||||
|
fmt.Println(`}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func printResultMapKeys(ctx context.Context, m map[string]interface{}, depth int) {
|
||||||
|
var isEnum bool
|
||||||
|
if m["value"] != nil && m["title"] != nil && m["options"] != nil {
|
||||||
|
isEnum = true
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, kv := range m {
|
||||||
|
fn := strcase.ToCamel(k)
|
||||||
|
|
||||||
|
switch k {
|
||||||
|
case "created_at", "updated_at", "archived_at":
|
||||||
|
pv := fmt.Sprintf("web.NewTimeResponse(ctx, actual.%s)", fn)
|
||||||
|
fmt.Printf("%s\"%s\": %s,\n", strings.Repeat("\t", depth), k, pv)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if sm, ok := kv.([]map[string]interface{}); ok {
|
||||||
|
fmt.Printf("%s\"%s\": []map[string]interface{}{\n", strings.Repeat("\t", depth), k)
|
||||||
|
|
||||||
|
for _, smv := range sm {
|
||||||
|
printResultMapKeys(ctx, smv, depth +1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("%s},\n", strings.Repeat("\t", depth))
|
||||||
|
} else if sm, ok := kv.(map[string]interface{}); ok {
|
||||||
|
fmt.Printf("%s\"%s\": map[string]interface{}{\n", strings.Repeat("\t", depth), k)
|
||||||
|
printResultMapKeys(ctx, sm, depth +1)
|
||||||
|
fmt.Printf("%s},\n", strings.Repeat("\t", depth))
|
||||||
|
} else {
|
||||||
|
var pv string
|
||||||
|
if isEnum {
|
||||||
|
jv, _ := json.Marshal(kv)
|
||||||
|
pv = string(jv)
|
||||||
|
} else {
|
||||||
|
pv = fmt.Sprintf("req.%s", fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("%s\"%s\": %s,\n", strings.Repeat("\t", depth), k, pv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -3,6 +3,7 @@ package account
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth"
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth"
|
||||||
@ -25,9 +26,6 @@ var (
|
|||||||
// ErrNotFound abstracts the mgo not found error.
|
// ErrNotFound abstracts the mgo not found error.
|
||||||
ErrNotFound = errors.New("Entity not found")
|
ErrNotFound = errors.New("Entity not found")
|
||||||
|
|
||||||
// ErrInvalidID occurs when an ID is not in a valid form.
|
|
||||||
ErrInvalidID = errors.New("ID is not in its proper form")
|
|
||||||
|
|
||||||
// ErrForbidden occurs when a user tries to do something that is forbidden to them according to our access control policies.
|
// ErrForbidden occurs when a user tries to do something that is forbidden to them according to our access control policies.
|
||||||
ErrForbidden = errors.New("Attempted action is not allowed")
|
ErrForbidden = errors.New("Attempted action is not allowed")
|
||||||
)
|
)
|
||||||
@ -243,6 +241,7 @@ func UniqueName(ctx context.Context, dbConn *sqlx.DB, name, accountId string) (b
|
|||||||
|
|
||||||
var existingId string
|
var existingId string
|
||||||
err := dbConn.QueryRowContext(ctx, queryStr, args...).Scan(&existingId)
|
err := dbConn.QueryRowContext(ctx, queryStr, args...).Scan(&existingId)
|
||||||
|
|
||||||
if err != nil && err != sql.ErrNoRows {
|
if err != nil && err != sql.ErrNoRows {
|
||||||
err = errors.Wrapf(err, "query - %s", query.String())
|
err = errors.Wrapf(err, "query - %s", query.String())
|
||||||
return false, err
|
return false, err
|
||||||
@ -261,8 +260,6 @@ func Create(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req Accoun
|
|||||||
span, ctx := tracer.StartSpanFromContext(ctx, "internal.account.Create")
|
span, ctx := tracer.StartSpanFromContext(ctx, "internal.account.Create")
|
||||||
defer span.Finish()
|
defer span.Finish()
|
||||||
|
|
||||||
v := validator.New()
|
|
||||||
|
|
||||||
// Validation email address is unique in the database.
|
// Validation email address is unique in the database.
|
||||||
uniq, err := UniqueName(ctx, dbConn, req.Name, "")
|
uniq, err := UniqueName(ctx, dbConn, req.Name, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -274,6 +271,8 @@ func Create(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req Accoun
|
|||||||
}
|
}
|
||||||
return uniq
|
return uniq
|
||||||
}
|
}
|
||||||
|
|
||||||
|
v := web.NewValidator()
|
||||||
v.RegisterValidation("unique", f)
|
v.RegisterValidation("unique", f)
|
||||||
|
|
||||||
// Validate the request.
|
// Validate the request.
|
||||||
@ -352,11 +351,11 @@ func Read(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, id string, i
|
|||||||
query.Where(query.Equal("id", id))
|
query.Where(query.Equal("id", id))
|
||||||
|
|
||||||
res, err := find(ctx, claims, dbConn, query, []interface{}{}, includedArchived)
|
res, err := find(ctx, claims, dbConn, query, []interface{}{}, includedArchived)
|
||||||
if err != nil {
|
if res == nil || len(res) == 0 {
|
||||||
return nil, err
|
|
||||||
} else if res == nil || len(res) == 0 {
|
|
||||||
err = errors.WithMessagef(ErrNotFound, "account %s not found", id)
|
err = errors.WithMessagef(ErrNotFound, "account %s not found", id)
|
||||||
return nil, err
|
return nil, err
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
u := res[0]
|
u := res[0]
|
||||||
|
|
||||||
@ -368,7 +367,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 := validator.New()
|
v := web.NewValidator()
|
||||||
|
|
||||||
// Validation name is unique in the database.
|
// Validation name is unique in the database.
|
||||||
if req.Name != nil {
|
if req.Name != nil {
|
||||||
@ -380,6 +379,7 @@ func Update(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req Accoun
|
|||||||
if fl.Field().String() == "invalid" {
|
if fl.Field().String() == "invalid" {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return uniq
|
return uniq
|
||||||
}
|
}
|
||||||
v.RegisterValidation("unique", f)
|
v.RegisterValidation("unique", f)
|
||||||
@ -495,7 +495,8 @@ 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.
|
||||||
err := validator.New().Struct(req)
|
v := web.NewValidator()
|
||||||
|
err := v.Struct(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -573,7 +574,8 @@ func Delete(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, accountID
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate the request.
|
// Validate the request.
|
||||||
err := validator.New().Struct(req)
|
v := web.NewValidator()
|
||||||
|
err := v.Struct(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -29,6 +29,7 @@ func Authenticate(authenticator *auth.Authenticator) web.Middleware {
|
|||||||
span, ctx := tracer.StartSpanFromContext(ctx, "internal.mid.Authenticate")
|
span, ctx := tracer.StartSpanFromContext(ctx, "internal.mid.Authenticate")
|
||||||
defer span.Finish()
|
defer span.Finish()
|
||||||
|
|
||||||
|
m := func() error {
|
||||||
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")
|
||||||
@ -48,6 +49,16 @@ func Authenticate(authenticator *auth.Authenticator) web.Middleware {
|
|||||||
// Add claims to the context so they can be retrieved later.
|
// Add claims to the context so they can be retrieved later.
|
||||||
ctx = context.WithValue(ctx, auth.Key, claims)
|
ctx = context.WithValue(ctx, auth.Key, claims)
|
||||||
|
|
||||||
|
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 after(ctx, w, r, params)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,6 +79,7 @@ func HasRole(roles ...string) web.Middleware {
|
|||||||
span, ctx := tracer.StartSpanFromContext(ctx, "internal.mid.HasRole")
|
span, ctx := tracer.StartSpanFromContext(ctx, "internal.mid.HasRole")
|
||||||
defer span.Finish()
|
defer span.Finish()
|
||||||
|
|
||||||
|
m := func() error {
|
||||||
claims, ok := ctx.Value(auth.Key).(auth.Claims)
|
claims, ok := ctx.Value(auth.Key).(auth.Claims)
|
||||||
if !ok {
|
if !ok {
|
||||||
// TODO(jlw) should this be a web.Shutdown?
|
// TODO(jlw) should this be a web.Shutdown?
|
||||||
@ -78,6 +90,16 @@ func HasRole(roles ...string) web.Middleware {
|
|||||||
return ErrForbidden
|
return ErrForbidden
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 after(ctx, w, r, params)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,3 +119,6 @@ func parseAuthHeader(bearerStr string) (string, error) {
|
|||||||
|
|
||||||
return split[1], nil
|
return split[1], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -28,9 +28,15 @@ func Errors(log *log.Logger) web.Middleware {
|
|||||||
log.Printf("%d : ERROR : %+v", span.Context().TraceID(), err)
|
log.Printf("%d : ERROR : %+v", span.Context().TraceID(), err)
|
||||||
|
|
||||||
// Respond to the error.
|
// Respond to the error.
|
||||||
|
if web.RequestIsJson(r) {
|
||||||
|
if err := web.RespondJsonError(ctx, w, err); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
if err := web.RespondError(ctx, w, err); err != nil {
|
if err := web.RespondError(ctx, w, err); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 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.
|
||||||
|
@ -2,6 +2,8 @@ package web
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"gopkg.in/go-playground/validator.v9"
|
||||||
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FieldError is used to indicate an error with a specific request field.
|
// FieldError is used to indicate an error with a specific request field.
|
||||||
@ -27,6 +29,12 @@ type Error struct {
|
|||||||
// NewRequestError wraps a provided error with an HTTP status code. This
|
// NewRequestError wraps a provided error with an HTTP status code. This
|
||||||
// function should be used when handlers encounter expected errors.
|
// function should be used when handlers encounter expected errors.
|
||||||
func NewRequestError(err error, status int) error {
|
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}
|
return &Error{err, status, nil}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,6 +60,35 @@ func NewShutdownError(message string) error {
|
|||||||
return &shutdown{message}
|
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
|
// IsShutdown checks to see if the shutdown error is contained
|
||||||
// in the specified error value.
|
// in the specified error value.
|
||||||
func IsShutdown(err error) bool {
|
func IsShutdown(err error) bool {
|
||||||
|
@ -36,7 +36,21 @@ func init() {
|
|||||||
en_translations.RegisterDefaultTranslations(validate, lang)
|
en_translations.RegisterDefaultTranslations(validate, lang)
|
||||||
|
|
||||||
// Use JSON tag names for errors instead of Go struct names.
|
// Use JSON tag names for errors instead of Go struct names.
|
||||||
validate.RegisterTagNameFunc(func(fld reflect.StructField) string {
|
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]
|
name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
|
||||||
if name == "-" {
|
if name == "-" {
|
||||||
return ""
|
return ""
|
||||||
@ -44,10 +58,7 @@ func init() {
|
|||||||
return name
|
return name
|
||||||
})
|
})
|
||||||
|
|
||||||
f := func(fl validator.FieldLevel) bool {
|
return v
|
||||||
return true
|
|
||||||
}
|
|
||||||
validate.RegisterValidation("unique", f)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
@ -56,45 +67,24 @@ func init() {
|
|||||||
// 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(r *http.Request, val interface{}) error {
|
||||||
|
|
||||||
if r.Method == http.MethodPost || r.Method == http.MethodPut || 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 NewRequestError(err, http.StatusBadRequest)
|
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")
|
||||||
return NewRequestError(err, http.StatusBadRequest)
|
return NewRequestError(err, http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := validate.Struct(val); err != nil {
|
if err := validate.Struct(val); err != nil {
|
||||||
|
verr, _ := NewValidationError(err)
|
||||||
// Use a type assertion to get the real error value.
|
return verr
|
||||||
verrors, ok := err.(validator.ValidationErrors)
|
|
||||||
if !ok {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -139,3 +129,30 @@ func ExtractWhereArgs(where string) (string, []interface{}, error) {
|
|||||||
|
|
||||||
return where, vals, nil
|
return where, vals, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func RequestIsJson(r *http.Request) bool {
|
||||||
|
if r == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if v := r.Header.Get("Content-type"); v != "" {
|
||||||
|
for _, hv := range strings.Split(v, ";") {
|
||||||
|
if strings.ToLower(hv) == "application/json" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if v := r.URL.Query().Get("ResponseFormat"); v != "" {
|
||||||
|
if strings.ToLower(v) == "json" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasSuffix(r.URL.Path, ".json") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -32,7 +32,12 @@ func RespondJsonError(ctx context.Context, w http.ResponseWriter, err error) err
|
|||||||
|
|
||||||
// If the error was of the type *Error, the handler has
|
// If the error was of the type *Error, the handler has
|
||||||
// a specific status code and error to return.
|
// a specific status code and error to return.
|
||||||
if webErr, ok := errors.Cause(err).(*Error); ok {
|
webErr, ok := errors.Cause(err).(*Error)
|
||||||
|
if !ok {
|
||||||
|
webErr, ok = err.(*Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok {
|
||||||
er := ErrorResponse{
|
er := ErrorResponse{
|
||||||
Error: webErr.Err.Error(),
|
Error: webErr.Err.Error(),
|
||||||
Fields: webErr.Fields,
|
Fields: webErr.Fields,
|
||||||
|
@ -4,12 +4,12 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth"
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth"
|
||||||
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web"
|
||||||
"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"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
|
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
|
||||||
"gopkg.in/go-playground/validator.v9"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -21,10 +21,9 @@ const (
|
|||||||
var (
|
var (
|
||||||
// ErrNotFound abstracts the postgres not found error.
|
// ErrNotFound abstracts the postgres not found error.
|
||||||
ErrNotFound = errors.New("Entity not found")
|
ErrNotFound = errors.New("Entity not found")
|
||||||
|
|
||||||
// ErrForbidden occurs when a user tries to do something that is forbidden to them according to our access control policies.
|
// ErrForbidden occurs when a user tries to do something that is forbidden to them according to our access control policies.
|
||||||
ErrForbidden = errors.New("Attempted action is not allowed")
|
ErrForbidden = errors.New("Attempted action is not allowed")
|
||||||
// ErrInvalidID occurs when an ID is not in a valid form.
|
|
||||||
ErrInvalidID = errors.New("ID is not in its proper form")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// projectMapColumns is the list of columns needed for mapRowsToProject
|
// projectMapColumns is the list of columns needed for mapRowsToProject
|
||||||
@ -193,14 +192,16 @@ func find(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, query *sqlbu
|
|||||||
func Read(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, id string, includedArchived bool) (*Project, error) {
|
func Read(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, id string, includedArchived bool) (*Project, error) {
|
||||||
span, ctx := tracer.StartSpanFromContext(ctx, "internal.project.Read")
|
span, ctx := tracer.StartSpanFromContext(ctx, "internal.project.Read")
|
||||||
defer span.Finish()
|
defer span.Finish()
|
||||||
|
|
||||||
// Filter base select query by id
|
// Filter base select query by id
|
||||||
query := selectQuery()
|
query := selectQuery()
|
||||||
query.Where(query.Equal("id", id))
|
query.Where(query.Equal("id", id))
|
||||||
|
|
||||||
res, err := find(ctx, claims, dbConn, query, []interface{}{}, includedArchived)
|
res, err := find(ctx, claims, dbConn, query, []interface{}{}, includedArchived)
|
||||||
if err != nil {
|
if res == nil || len(res) == 0 {
|
||||||
|
err = errors.WithMessagef(ErrNotFound, "account %s not found", id)
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if res == nil || len(res) == 0 {
|
} else if err != nil {
|
||||||
err = errors.WithMessagef(ErrNotFound, "project %s not found", id)
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -231,8 +232,8 @@ func Create(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req Projec
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
v := validator.New()
|
|
||||||
// Validate the request.
|
// Validate the request.
|
||||||
|
v := web.NewValidator()
|
||||||
err := v.Struct(req)
|
err := v.Struct(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -301,8 +302,9 @@ func Create(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req Projec
|
|||||||
func Update(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req ProjectUpdateRequest, now time.Time) error {
|
func Update(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req ProjectUpdateRequest, now time.Time) error {
|
||||||
span, ctx := tracer.StartSpanFromContext(ctx, "internal.project.Update")
|
span, ctx := tracer.StartSpanFromContext(ctx, "internal.project.Update")
|
||||||
defer span.Finish()
|
defer span.Finish()
|
||||||
v := validator.New()
|
|
||||||
// Validate the request.
|
// Validate the request.
|
||||||
|
v := web.NewValidator()
|
||||||
err := v.Struct(req)
|
err := v.Struct(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -372,7 +374,8 @@ 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.
|
||||||
err := validator.New().Struct(req)
|
v := web.NewValidator()
|
||||||
|
err := v.Struct(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -418,12 +421,15 @@ func Archive(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req Proje
|
|||||||
func Delete(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, id string) error {
|
func Delete(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, id string) error {
|
||||||
span, ctx := tracer.StartSpanFromContext(ctx, "internal.project.Delete")
|
span, ctx := tracer.StartSpanFromContext(ctx, "internal.project.Delete")
|
||||||
defer span.Finish()
|
defer span.Finish()
|
||||||
|
|
||||||
// Defines the struct to apply validation
|
// Defines the struct to apply validation
|
||||||
req := struct {
|
req := struct {
|
||||||
ID string `validate:"required,uuid"`
|
ID string `validate:"required,uuid"`
|
||||||
}{}
|
}{}
|
||||||
|
|
||||||
// Validate the request.
|
// Validate the request.
|
||||||
err := validator.New().Struct(req)
|
v := web.NewValidator()
|
||||||
|
err := v.Struct(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package signup
|
package signup
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/account"
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/account"
|
||||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/user"
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/user"
|
||||||
)
|
)
|
||||||
@ -31,8 +32,33 @@ type SignupUser struct {
|
|||||||
PasswordConfirm string `json:"password_confirm" validate:"eqfield=Password" example:"SecretString"`
|
PasswordConfirm string `json:"password_confirm" validate:"eqfield=Password" example:"SecretString"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// SignupResponse response signup with created account and user.
|
// SignupResult response signup with created account and user.
|
||||||
type SignupResponse struct {
|
type SignupResult struct {
|
||||||
Account *account.Account `json:"account"`
|
Account *account.Account `json:"account"`
|
||||||
User *user.User `json:"user"`
|
User *user.User `json:"user"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SignupResponse represents the user and account created for signup that is returned for display.
|
||||||
|
type SignupResponse struct {
|
||||||
|
Account *account.AccountResponse `json:"account"`
|
||||||
|
User *user.UserResponse `json:"user"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response transforms SignupResult to SignupResponse that is used for display.
|
||||||
|
// Additional filtering by context values or translations could be applied.
|
||||||
|
func (m *SignupResult) Response(ctx context.Context) *SignupResponse {
|
||||||
|
if m == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
r := &SignupResponse{}
|
||||||
|
if m.Account != nil {
|
||||||
|
r.Account = m.Account.Response(ctx)
|
||||||
|
}
|
||||||
|
if m.User != nil {
|
||||||
|
r.User = m.User.Response(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ package signup
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/account"
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/account"
|
||||||
@ -15,12 +16,10 @@ import (
|
|||||||
|
|
||||||
// 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) (*SignupResponse, error) {
|
func Signup(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req SignupRequest, now time.Time) (*SignupResult, error) {
|
||||||
span, ctx := tracer.StartSpanFromContext(ctx, "internal.signup.Signup")
|
span, ctx := tracer.StartSpanFromContext(ctx, "internal.signup.Signup")
|
||||||
defer span.Finish()
|
defer span.Finish()
|
||||||
|
|
||||||
v := validator.New()
|
|
||||||
|
|
||||||
// Validate the user email address is unique in the database.
|
// Validate the user email address is unique in the database.
|
||||||
uniqEmail, err := user.UniqueEmail(ctx, dbConn, req.User.Email, "")
|
uniqEmail, err := user.UniqueEmail(ctx, dbConn, req.User.Email, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -33,6 +32,7 @@ func Signup(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req Signup
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
f := func(fl validator.FieldLevel) bool {
|
f := func(fl validator.FieldLevel) bool {
|
||||||
if fl.Field().String() == "invalid" {
|
if fl.Field().String() == "invalid" {
|
||||||
return false
|
return false
|
||||||
@ -40,14 +40,16 @@ func Signup(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req Signup
|
|||||||
|
|
||||||
var uniq bool
|
var uniq bool
|
||||||
switch fl.FieldName() {
|
switch fl.FieldName() {
|
||||||
case "Name":
|
case "Name", "name":
|
||||||
uniq = uniqName
|
uniq = uniqName
|
||||||
case "Email":
|
case "Email", "email":
|
||||||
uniq = uniqEmail
|
uniq = uniqEmail
|
||||||
}
|
}
|
||||||
|
|
||||||
return uniq
|
return uniq
|
||||||
}
|
}
|
||||||
|
|
||||||
|
v := web.NewValidator()
|
||||||
v.RegisterValidation("unique", f)
|
v.RegisterValidation("unique", f)
|
||||||
|
|
||||||
// Validate the request.
|
// Validate the request.
|
||||||
@ -56,7 +58,7 @@ func Signup(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req Signup
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var resp SignupResponse
|
var resp SignupResult
|
||||||
|
|
||||||
// UserCreateRequest contains information needed to create a new User.
|
// UserCreateRequest contains information needed to create a new User.
|
||||||
userReq := user.UserCreateRequest{
|
userReq := user.UserCreateRequest{
|
||||||
|
@ -32,12 +32,12 @@ func TestSignupValidation(t *testing.T) {
|
|||||||
var userTests = []struct {
|
var userTests = []struct {
|
||||||
name string
|
name string
|
||||||
req SignupRequest
|
req SignupRequest
|
||||||
expected func(req SignupRequest, res *SignupResponse) *SignupResponse
|
expected func(req SignupRequest, res *SignupResult) *SignupResult
|
||||||
error error
|
error error
|
||||||
}{
|
}{
|
||||||
{"Required Fields",
|
{"Required Fields",
|
||||||
SignupRequest{},
|
SignupRequest{},
|
||||||
func(req SignupRequest, res *SignupResponse) *SignupResponse {
|
func(req SignupRequest, res *SignupResult) *SignupResult {
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
errors.New("Key: 'SignupRequest.Account.Name' Error:Field validation for 'Name' failed on the 'required' tag\n" +
|
errors.New("Key: 'SignupRequest.Account.Name' Error:Field validation for 'Name' failed on the 'required' tag\n" +
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web"
|
||||||
"github.com/dgrijalva/jwt-go"
|
"github.com/dgrijalva/jwt-go"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@ -15,7 +16,6 @@ import (
|
|||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
|
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
|
||||||
"gopkg.in/go-playground/validator.v9"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// TokenGenerator is the behavior we need in our Authenticate to generate tokens for
|
// TokenGenerator is the behavior we need in our Authenticate to generate tokens for
|
||||||
@ -80,7 +80,8 @@ func SwitchAccount(ctx context.Context, dbConn *sqlx.DB, tknGen TokenGenerator,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate the request.
|
// Validate the request.
|
||||||
err := validator.New().Struct(req)
|
v := web.NewValidator()
|
||||||
|
err := v.Struct(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Token{}, err
|
return Token{}, err
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ package user
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth"
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth"
|
||||||
@ -28,9 +29,6 @@ var (
|
|||||||
// ErrNotFound abstracts the mgo not found error.
|
// ErrNotFound abstracts the mgo not found error.
|
||||||
ErrNotFound = errors.New("Entity not found")
|
ErrNotFound = errors.New("Entity not found")
|
||||||
|
|
||||||
// ErrInvalidID occurs when an ID is not in a valid form.
|
|
||||||
ErrInvalidID = errors.New("ID is not in its proper form")
|
|
||||||
|
|
||||||
// ErrForbidden occurs when a user tries to do something that is forbidden to them according to our access control policies.
|
// ErrForbidden occurs when a user tries to do something that is forbidden to them according to our access control policies.
|
||||||
ErrForbidden = errors.New("Attempted action is not allowed")
|
ErrForbidden = errors.New("Attempted action is not allowed")
|
||||||
|
|
||||||
@ -261,8 +259,6 @@ func Create(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req UserCr
|
|||||||
span, ctx := tracer.StartSpanFromContext(ctx, "internal.user.Create")
|
span, ctx := tracer.StartSpanFromContext(ctx, "internal.user.Create")
|
||||||
defer span.Finish()
|
defer span.Finish()
|
||||||
|
|
||||||
v := validator.New()
|
|
||||||
|
|
||||||
// Validation email address is unique in the database.
|
// Validation email address is unique in the database.
|
||||||
uniq, err := UniqueEmail(ctx, dbConn, req.Email, "")
|
uniq, err := UniqueEmail(ctx, dbConn, req.Email, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -274,6 +270,8 @@ func Create(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req UserCr
|
|||||||
}
|
}
|
||||||
return uniq
|
return uniq
|
||||||
}
|
}
|
||||||
|
|
||||||
|
v := web.NewValidator()
|
||||||
v.RegisterValidation("unique", f)
|
v.RegisterValidation("unique", f)
|
||||||
|
|
||||||
// Validate the request.
|
// Validate the request.
|
||||||
@ -356,11 +354,11 @@ func Read(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, id string, i
|
|||||||
query.Where(query.Equal("id", id))
|
query.Where(query.Equal("id", id))
|
||||||
|
|
||||||
res, err := find(ctx, claims, dbConn, query, []interface{}{}, includedArchived)
|
res, err := find(ctx, claims, dbConn, query, []interface{}{}, includedArchived)
|
||||||
if err != nil {
|
if res == nil || len(res) == 0 {
|
||||||
return nil, err
|
|
||||||
} else if res == nil || len(res) == 0 {
|
|
||||||
err = errors.WithMessagef(ErrNotFound, "user %s not found", id)
|
err = errors.WithMessagef(ErrNotFound, "user %s not found", id)
|
||||||
return nil, err
|
return nil, err
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
u := res[0]
|
u := res[0]
|
||||||
|
|
||||||
@ -372,7 +370,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 := validator.New()
|
v := web.NewValidator()
|
||||||
|
|
||||||
// Validation email address is unique in the database.
|
// Validation email address is unique in the database.
|
||||||
if req.Email != nil {
|
if req.Email != nil {
|
||||||
@ -458,7 +456,8 @@ func UpdatePassword(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, re
|
|||||||
defer span.Finish()
|
defer span.Finish()
|
||||||
|
|
||||||
// Validate the request.
|
// Validate the request.
|
||||||
err := validator.New().Struct(req)
|
v := web.NewValidator()
|
||||||
|
err := v.Struct(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -526,7 +525,8 @@ 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.
|
||||||
err := validator.New().Struct(req)
|
v := web.NewValidator()
|
||||||
|
err := v.Struct(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -604,7 +604,8 @@ func Delete(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, userID str
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate the request.
|
// Validate the request.
|
||||||
err := validator.New().Struct(req)
|
v := web.NewValidator()
|
||||||
|
err := v.Struct(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/account"
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/account"
|
||||||
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth"
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth"
|
||||||
@ -12,16 +13,12 @@ import (
|
|||||||
"github.com/pborman/uuid"
|
"github.com/pborman/uuid"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
|
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
|
||||||
"gopkg.in/go-playground/validator.v9"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// ErrNotFound abstracts the mgo not found error.
|
// ErrNotFound abstracts the mgo not found error.
|
||||||
ErrNotFound = errors.New("Entity not found")
|
ErrNotFound = errors.New("Entity not found")
|
||||||
|
|
||||||
// ErrInvalidID occurs when an ID is not in a valid form.
|
|
||||||
ErrInvalidID = errors.New("ID is not in its proper form")
|
|
||||||
|
|
||||||
// ErrForbidden occurs when a user tries to do something that is forbidden to them according to our access control policies.
|
// ErrForbidden occurs when a user tries to do something that is forbidden to them according to our access control policies.
|
||||||
ErrForbidden = errors.New("Attempted action is not allowed")
|
ErrForbidden = errors.New("Attempted action is not allowed")
|
||||||
)
|
)
|
||||||
@ -64,8 +61,6 @@ func mapAccountError(err error) error {
|
|||||||
switch errors.Cause(err) {
|
switch errors.Cause(err) {
|
||||||
case account.ErrNotFound:
|
case account.ErrNotFound:
|
||||||
err = ErrNotFound
|
err = ErrNotFound
|
||||||
case account.ErrInvalidID:
|
|
||||||
err = ErrInvalidID
|
|
||||||
case account.ErrForbidden:
|
case account.ErrForbidden:
|
||||||
err = ErrForbidden
|
err = ErrForbidden
|
||||||
}
|
}
|
||||||
@ -206,7 +201,8 @@ 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.
|
||||||
err := validator.New().Struct(req)
|
v := web.NewValidator()
|
||||||
|
err := v.Struct(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -303,10 +299,10 @@ func Read(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, id string, i
|
|||||||
query.Where(query.Equal("id", id))
|
query.Where(query.Equal("id", id))
|
||||||
|
|
||||||
res, err := find(ctx, claims, dbConn, query, []interface{}{}, includedArchived)
|
res, err := find(ctx, claims, dbConn, query, []interface{}{}, includedArchived)
|
||||||
if err != nil {
|
if res == nil || len(res) == 0 {
|
||||||
|
err = errors.WithMessagef(ErrNotFound, "account %s not found", id)
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if res == nil || len(res) == 0 {
|
} else if err != nil {
|
||||||
err = errors.WithMessagef(ErrNotFound, "user account %s not found", id)
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
u := res[0]
|
u := res[0]
|
||||||
@ -320,7 +316,8 @@ 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.
|
||||||
err := validator.New().Struct(req)
|
v := web.NewValidator()
|
||||||
|
err := v.Struct(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -392,7 +389,8 @@ 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.
|
||||||
err := validator.New().Struct(req)
|
v := web.NewValidator()
|
||||||
|
err := v.Struct(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -443,7 +441,8 @@ 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.
|
||||||
err := validator.New().Struct(req)
|
v := web.NewValidator()
|
||||||
|
err := v.Struct(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user