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
Completed API documentation for swagger
This commit is contained in:
@ -6,7 +6,11 @@ accelerator@geeksinthewoods.com.com
|
|||||||
|
|
||||||
## Description
|
## Description
|
||||||
|
|
||||||
Service exposes a JSON api.
|
Web API is a client facing API. Standard response format is JSON.
|
||||||
|
|
||||||
|
**Not all CRUD methods are exposed as endpoints.** Only endpoints that clients may need should be exposed. Internal
|
||||||
|
services should communicate directly with the business logic packages or a new API should be created to support. This
|
||||||
|
separation should help decouple client integrations from internal application development.
|
||||||
|
|
||||||
|
|
||||||
## Local Installation
|
## Local Installation
|
||||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -10,6 +10,7 @@ import (
|
|||||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web"
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web"
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"gopkg.in/go-playground/validator.v9"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Account represents the Account API method handler set.
|
// Account represents the Account API method handler set.
|
||||||
@ -19,39 +20,19 @@ type Account struct {
|
|||||||
// ADD OTHER STATE LIKE THE LOGGER AND CONFIG HERE.
|
// ADD OTHER STATE LIKE THE LOGGER AND CONFIG HERE.
|
||||||
}
|
}
|
||||||
|
|
||||||
// List returns all the existing accounts in the system.
|
|
||||||
func (a *Account) 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 account.AccountFindRequest
|
|
||||||
if err := web.Decode(r, &req); err != nil {
|
|
||||||
return errors.Wrap(err, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := account.Find(ctx, claims, a.MasterDB, req)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return web.RespondJson(ctx, w, res, http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read godoc
|
// Read godoc
|
||||||
// @Summary Read returns the specified account from the system.
|
// @Summary Get account by ID
|
||||||
// @Description get string by ID
|
// @Description Read returns the specified account from the system.
|
||||||
// @Tags account
|
// @Tags account
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Security OAuth2Password
|
// @Security OAuth2Password
|
||||||
// @Param id path string true "Account ID"
|
// @Param id path string true "Account ID"
|
||||||
// @Success 200 {object} account.Account
|
// @Success 200 {object} account.AccountResponse
|
||||||
// @Header 200 {string} Token "qwerty"
|
// @Failure 400 {object} web.ErrorResponse
|
||||||
// @Failure 400 {object} web.Error
|
// @Failure 403 {object} web.ErrorResponse
|
||||||
// @Failure 403 {object} web.Error
|
// @Failure 404 {object} web.ErrorResponse
|
||||||
// @Failure 404 {object} web.Error
|
// @Failure 500 {object} web.ErrorResponse
|
||||||
// @Router /accounts/{id} [get]
|
// @Router /accounts/{id} [get]
|
||||||
func (a *Account) Read(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
func (a *Account) Read(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||||
claims, ok := ctx.Value(auth.Key).(auth.Claims)
|
claims, ok := ctx.Value(auth.Key).(auth.Claims)
|
||||||
@ -82,40 +63,23 @@ func (a *Account) Read(ctx context.Context, w http.ResponseWriter, r *http.Reque
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return web.RespondJson(ctx, w, res, http.StatusOK)
|
return web.RespondJson(ctx, w, res.Response(ctx), http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create inserts a new account into the system.
|
// Read godoc
|
||||||
func (a *Account) Create(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
// @Summary Update account by ID
|
||||||
v, ok := ctx.Value(web.KeyValues).(*web.Values)
|
// @Description Update updates the specified account in the system.
|
||||||
if !ok {
|
// @Tags account
|
||||||
return web.NewShutdownError("web value missing from context")
|
// @Accept json
|
||||||
}
|
// @Produce json
|
||||||
|
// @Security OAuth2Password
|
||||||
claims, ok := ctx.Value(auth.Key).(auth.Claims)
|
// @Param data body account.AccountUpdateRequest true "Update fields"
|
||||||
if !ok {
|
// @Success 201
|
||||||
return errors.New("claims missing from context")
|
// @Failure 400 {object} web.ErrorResponse
|
||||||
}
|
// @Failure 403 {object} web.ErrorResponse
|
||||||
|
// @Failure 404 {object} web.ErrorResponse
|
||||||
var req account.AccountCreateRequest
|
// @Failure 500 {object} web.ErrorResponse
|
||||||
if err := web.Decode(r, &req); err != nil {
|
// @Router /accounts [patch]
|
||||||
return errors.Wrap(err, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := account.Create(ctx, claims, a.MasterDB, req, v.Now)
|
|
||||||
if err != nil {
|
|
||||||
switch err {
|
|
||||||
case account.ErrForbidden:
|
|
||||||
return web.NewRequestError(err, http.StatusForbidden)
|
|
||||||
default:
|
|
||||||
return errors.Wrapf(err, "User: %+v", &req)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return web.RespondJson(ctx, w, res, http.StatusCreated)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update updates the specified account in the system.
|
|
||||||
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 {
|
||||||
v, ok := ctx.Value(web.KeyValues).(*web.Values)
|
v, ok := ctx.Value(web.KeyValues).(*web.Values)
|
||||||
if !ok {
|
if !ok {
|
||||||
@ -129,9 +93,14 @@ 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 {
|
||||||
return errors.Wrap(err, "")
|
err = errors.WithStack(err)
|
||||||
|
|
||||||
|
_, ok := err.(validator.ValidationErrors)
|
||||||
|
if ok {
|
||||||
|
return web.NewRequestError(err, http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
req.ID = params["id"]
|
|
||||||
|
|
||||||
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 {
|
||||||
@ -143,62 +112,14 @@ func (a *Account) Update(ctx context.Context, w http.ResponseWriter, r *http.Req
|
|||||||
case account.ErrForbidden:
|
case account.ErrForbidden:
|
||||||
return web.NewRequestError(err, http.StatusForbidden)
|
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 Account: %+v", params["id"], &req)
|
return errors.Wrapf(err, "Id: %s Account: %+v", params["id"], &req)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return web.RespondJson(ctx, w, nil, http.StatusNoContent)
|
return web.RespondJson(ctx, w, nil, http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Archive soft-deletes the specified account from the system.
|
|
||||||
func (a *Account) 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")
|
|
||||||
}
|
|
||||||
|
|
||||||
err := account.Archive(ctx, claims, a.MasterDB, params["id"], v.Now)
|
|
||||||
if err != nil {
|
|
||||||
switch err {
|
|
||||||
case account.ErrInvalidID:
|
|
||||||
return web.NewRequestError(err, http.StatusBadRequest)
|
|
||||||
case account.ErrNotFound:
|
|
||||||
return web.NewRequestError(err, http.StatusNotFound)
|
|
||||||
case account.ErrForbidden:
|
|
||||||
return web.NewRequestError(err, http.StatusForbidden)
|
|
||||||
default:
|
|
||||||
return errors.Wrapf(err, "Id: %s", params["id"])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return web.RespondJson(ctx, w, nil, http.StatusNoContent)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete removes the specified account from the system.
|
|
||||||
func (a *Account) 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")
|
|
||||||
}
|
|
||||||
|
|
||||||
err := account.Delete(ctx, claims, a.MasterDB, params["id"])
|
|
||||||
if err != nil {
|
|
||||||
switch err {
|
|
||||||
case account.ErrInvalidID:
|
|
||||||
return web.NewRequestError(err, http.StatusBadRequest)
|
|
||||||
case account.ErrNotFound:
|
|
||||||
return web.NewRequestError(err, http.StatusNotFound)
|
|
||||||
case account.ErrForbidden:
|
|
||||||
return web.NewRequestError(err, http.StatusForbidden)
|
|
||||||
default:
|
|
||||||
return errors.Wrapf(err, "Id: %s", params["id"])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return web.RespondJson(ctx, w, nil, http.StatusNoContent)
|
|
||||||
}
|
|
||||||
|
@ -4,12 +4,14 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"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"
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web"
|
||||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/project"
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/project"
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"gopkg.in/go-playground/validator.v9"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Project represents the Project API method handler set.
|
// Project represents the Project API method handler set.
|
||||||
@ -19,7 +21,23 @@ type Project struct {
|
|||||||
// ADD OTHER STATE LIKE THE LOGGER IF NEEDED.
|
// ADD OTHER STATE LIKE THE LOGGER IF NEEDED.
|
||||||
}
|
}
|
||||||
|
|
||||||
// List returns all the existing projects in the system.
|
// Find godoc
|
||||||
|
// @Summary List projects
|
||||||
|
// @Description Find returns the existing projects in the system.
|
||||||
|
// @Tags project
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security OAuth2Password
|
||||||
|
// @Param where query string false "Filter string, example: name = 'Moon Launch'"
|
||||||
|
// @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} project.ProjectResponse
|
||||||
|
// @Failure 400 {object} web.ErrorResponse
|
||||||
|
// @Failure 403 {object} web.ErrorResponse
|
||||||
|
// @Failure 500 {object} web.ErrorResponse
|
||||||
|
// @Router /project [get]
|
||||||
func (p *Project) Find(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
func (p *Project) Find(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||||
claims, ok := ctx.Value(auth.Key).(auth.Claims)
|
claims, ok := ctx.Value(auth.Key).(auth.Claims)
|
||||||
if !ok {
|
if !ok {
|
||||||
@ -27,8 +45,57 @@ func (p *Project) Find(ctx context.Context, w http.ResponseWriter, r *http.Reque
|
|||||||
}
|
}
|
||||||
|
|
||||||
var req project.ProjectFindRequest
|
var req project.ProjectFindRequest
|
||||||
if err := web.Decode(r, &req); err != nil {
|
|
||||||
return errors.Wrap(err, "")
|
// 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.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.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.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.NewRequestError(err, http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
req.IncludedArchived = b
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := project.Find(ctx, claims, p.MasterDB, req)
|
res, err := project.Find(ctx, claims, p.MasterDB, req)
|
||||||
@ -36,10 +103,28 @@ func (p *Project) Find(ctx context.Context, w http.ResponseWriter, r *http.Reque
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return web.RespondJson(ctx, w, res, http.StatusOK)
|
var resp []*project.ProjectResponse
|
||||||
|
for _, m := range res {
|
||||||
|
resp = append(resp, m.Response(ctx))
|
||||||
|
}
|
||||||
|
|
||||||
|
return web.RespondJson(ctx, w, resp, http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read returns the specified project from the system.
|
// Read godoc
|
||||||
|
// @Summary Get project by ID.
|
||||||
|
// @Description Read returns the specified project from the system.
|
||||||
|
// @Tags project
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security OAuth2Password
|
||||||
|
// @Param id path string true "Project ID"
|
||||||
|
// @Success 200 {object} project.ProjectResponse
|
||||||
|
// @Failure 400 {object} web.ErrorResponse
|
||||||
|
// @Failure 403 {object} web.ErrorResponse
|
||||||
|
// @Failure 404 {object} web.ErrorResponse
|
||||||
|
// @Failure 500 {object} web.ErrorResponse
|
||||||
|
// @Router /projects/{id} [get]
|
||||||
func (p *Project) Read(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
func (p *Project) Read(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||||
claims, ok := ctx.Value(auth.Key).(auth.Claims)
|
claims, ok := ctx.Value(auth.Key).(auth.Claims)
|
||||||
if !ok {
|
if !ok {
|
||||||
@ -69,10 +154,23 @@ func (p *Project) Read(ctx context.Context, w http.ResponseWriter, r *http.Reque
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return web.RespondJson(ctx, w, res, http.StatusOK)
|
return web.RespondJson(ctx, w, res.Response(ctx), http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create inserts a new project into the system.
|
// Create godoc
|
||||||
|
// @Summary Create new project.
|
||||||
|
// @Description Create inserts a new project into the system.
|
||||||
|
// @Tags project
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security OAuth2Password
|
||||||
|
// @Param data body project.ProjectCreateRequest true "Project details"
|
||||||
|
// @Success 200 {object} project.ProjectResponse
|
||||||
|
// @Failure 400 {object} web.ErrorResponse
|
||||||
|
// @Failure 403 {object} web.ErrorResponse
|
||||||
|
// @Failure 404 {object} web.ErrorResponse
|
||||||
|
// @Failure 500 {object} web.ErrorResponse
|
||||||
|
// @Router /projects [post]
|
||||||
func (p *Project) Create(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
func (p *Project) Create(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)
|
||||||
if !ok {
|
if !ok {
|
||||||
@ -86,7 +184,13 @@ 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 {
|
||||||
return errors.Wrap(err, "")
|
err = errors.WithStack(err)
|
||||||
|
|
||||||
|
_, ok := err.(validator.ValidationErrors)
|
||||||
|
if ok {
|
||||||
|
return web.NewRequestError(err, http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := project.Create(ctx, claims, p.MasterDB, req, v.Now)
|
res, err := project.Create(ctx, claims, p.MasterDB, req, v.Now)
|
||||||
@ -95,14 +199,31 @@ func (p *Project) Create(ctx context.Context, w http.ResponseWriter, r *http.Req
|
|||||||
case project.ErrForbidden:
|
case project.ErrForbidden:
|
||||||
return web.NewRequestError(err, http.StatusForbidden)
|
return web.NewRequestError(err, http.StatusForbidden)
|
||||||
default:
|
default:
|
||||||
|
_, ok := err.(validator.ValidationErrors)
|
||||||
|
if ok {
|
||||||
|
return web.NewRequestError(err, http.StatusBadRequest)
|
||||||
|
}
|
||||||
return errors.Wrapf(err, "Project: %+v", &req)
|
return errors.Wrapf(err, "Project: %+v", &req)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return web.RespondJson(ctx, w, res, http.StatusCreated)
|
return web.RespondJson(ctx, w, res.Response(ctx), http.StatusCreated)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update updates the specified project in the system.
|
// Read godoc
|
||||||
|
// @Summary Update project by ID
|
||||||
|
// @Description Update updates the specified project in the system.
|
||||||
|
// @Tags project
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security OAuth2Password
|
||||||
|
// @Param data body project.ProjectUpdateRequest true "Update fields"
|
||||||
|
// @Success 201
|
||||||
|
// @Failure 400 {object} web.ErrorResponse
|
||||||
|
// @Failure 403 {object} web.ErrorResponse
|
||||||
|
// @Failure 404 {object} web.ErrorResponse
|
||||||
|
// @Failure 500 {object} web.ErrorResponse
|
||||||
|
// @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 {
|
||||||
v, ok := ctx.Value(web.KeyValues).(*web.Values)
|
v, ok := ctx.Value(web.KeyValues).(*web.Values)
|
||||||
if !ok {
|
if !ok {
|
||||||
@ -116,9 +237,14 @@ 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 {
|
||||||
return errors.Wrap(err, "")
|
err = errors.WithStack(err)
|
||||||
|
|
||||||
|
_, ok := err.(validator.ValidationErrors)
|
||||||
|
if ok {
|
||||||
|
return web.NewRequestError(err, http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
req.ID = params["id"]
|
|
||||||
|
|
||||||
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 {
|
||||||
@ -130,14 +256,31 @@ func (p *Project) Update(ctx context.Context, w http.ResponseWriter, r *http.Req
|
|||||||
case project.ErrForbidden:
|
case project.ErrForbidden:
|
||||||
return web.NewRequestError(err, http.StatusForbidden)
|
return web.NewRequestError(err, http.StatusForbidden)
|
||||||
default:
|
default:
|
||||||
return errors.Wrapf(err, "ID: %s Update: %+v", params["id"], req)
|
_, ok := err.(validator.ValidationErrors)
|
||||||
|
if ok {
|
||||||
|
return web.NewRequestError(err, http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
return errors.Wrapf(err, "ID: %s Update: %+v", req.ID, req)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return web.RespondJson(ctx, w, nil, http.StatusNoContent)
|
return web.RespondJson(ctx, w, nil, http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Archive soft-deletes the specified project from the system.
|
// Read godoc
|
||||||
|
// @Summary Archive project by ID
|
||||||
|
// @Description Archive soft-deletes the specified project from the system.
|
||||||
|
// @Tags project
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security OAuth2Password
|
||||||
|
// @Param data body project.ProjectArchiveRequest true "Update fields"
|
||||||
|
// @Success 201
|
||||||
|
// @Failure 400 {object} web.ErrorResponse
|
||||||
|
// @Failure 403 {object} web.ErrorResponse
|
||||||
|
// @Failure 404 {object} web.ErrorResponse
|
||||||
|
// @Failure 500 {object} web.ErrorResponse
|
||||||
|
// @Router /projects/archive [patch]
|
||||||
func (p *Project) Archive(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
func (p *Project) Archive(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)
|
||||||
if !ok {
|
if !ok {
|
||||||
@ -149,7 +292,18 @@ func (p *Project) Archive(ctx context.Context, w http.ResponseWriter, r *http.Re
|
|||||||
return errors.New("claims missing from context")
|
return errors.New("claims missing from context")
|
||||||
}
|
}
|
||||||
|
|
||||||
err := project.Archive(ctx, claims, p.MasterDB, params["id"], v.Now)
|
var req project.ProjectArchiveRequest
|
||||||
|
if err := web.Decode(r, &req); err != nil {
|
||||||
|
err = errors.WithStack(err)
|
||||||
|
|
||||||
|
_, ok := err.(validator.ValidationErrors)
|
||||||
|
if ok {
|
||||||
|
return web.NewRequestError(err, http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err := project.Archive(ctx, claims, p.MasterDB, req, v.Now)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch err {
|
switch err {
|
||||||
case project.ErrInvalidID:
|
case project.ErrInvalidID:
|
||||||
@ -159,14 +313,32 @@ func (p *Project) Archive(ctx context.Context, w http.ResponseWriter, r *http.Re
|
|||||||
case project.ErrForbidden:
|
case project.ErrForbidden:
|
||||||
return web.NewRequestError(err, http.StatusForbidden)
|
return web.NewRequestError(err, http.StatusForbidden)
|
||||||
default:
|
default:
|
||||||
return errors.Wrapf(err, "Id: %s", params["id"])
|
_, ok := err.(validator.ValidationErrors)
|
||||||
|
if ok {
|
||||||
|
return web.NewRequestError(err, http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.Wrapf(err, "Id: %s", req.ID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return web.RespondJson(ctx, w, nil, http.StatusNoContent)
|
return web.RespondJson(ctx, w, nil, http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete removes the specified project from the system.
|
// Delete godoc
|
||||||
|
// @Summary Delete project by ID
|
||||||
|
// @Description Delete removes the specified project from the system.
|
||||||
|
// @Tags project
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security OAuth2Password
|
||||||
|
// @Param id path string true "Project ID"
|
||||||
|
// @Success 201
|
||||||
|
// @Failure 400 {object} web.ErrorResponse
|
||||||
|
// @Failure 403 {object} web.ErrorResponse
|
||||||
|
// @Failure 404 {object} web.ErrorResponse
|
||||||
|
// @Failure 500 {object} web.ErrorResponse
|
||||||
|
// @Router /projects/{id} [delete]
|
||||||
func (p *Project) Delete(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
func (p *Project) Delete(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||||
claims, ok := ctx.Value(auth.Key).(auth.Claims)
|
claims, ok := ctx.Value(auth.Key).(auth.Claims)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
@ -30,14 +30,15 @@ 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/:id", u.Update, mid.Authenticate(authenticator))
|
app.Handle("PATCH", "/v1/users", u.Update, mid.Authenticate(authenticator))
|
||||||
app.Handle("PATCH", "/v1/users/:id/password", u.UpdatePassword, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
|
app.Handle("PATCH", "/v1/users/password", u.UpdatePassword, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
|
||||||
app.Handle("PATCH", "/v1/users/:id/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/:accountId", u.SwitchAccount, mid.Authenticate(authenticator))
|
app.Handle("PATCH", "/v1/users/switch-account/:account_id", u.SwitchAccount, mid.Authenticate(authenticator))
|
||||||
|
|
||||||
// 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)
|
||||||
@ -46,12 +47,8 @@ func API(shutdown chan os.Signal, log *log.Logger, masterDB *sqlx.DB, redis *red
|
|||||||
a := Account{
|
a := Account{
|
||||||
MasterDB: masterDB,
|
MasterDB: masterDB,
|
||||||
}
|
}
|
||||||
app.Handle("GET", "/v1/accounts", a.Find, mid.Authenticate(authenticator))
|
|
||||||
app.Handle("POST", "/v1/accounts", a.Create, mid.Authenticate(authenticator))
|
|
||||||
app.Handle("GET", "/v1/accounts/:id", a.Read, mid.Authenticate(authenticator))
|
app.Handle("GET", "/v1/accounts/:id", a.Read, mid.Authenticate(authenticator))
|
||||||
app.Handle("PATCH", "/v1/accounts/:id", a.Update, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
|
app.Handle("PATCH", "/v1/accounts", a.Update, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
|
||||||
app.Handle("PATCH", "/v1/accounts/:id/archive", a.Archive, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
|
|
||||||
app.Handle("DELETE", "/v1/accounts/:id", a.Delete, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
|
|
||||||
|
|
||||||
// Register signup endpoints.
|
// Register signup endpoints.
|
||||||
s := Signup{
|
s := Signup{
|
||||||
@ -66,8 +63,8 @@ func API(shutdown chan os.Signal, log *log.Logger, masterDB *sqlx.DB, redis *red
|
|||||||
app.Handle("GET", "/v1/projects", p.Find, mid.Authenticate(authenticator))
|
app.Handle("GET", "/v1/projects", p.Find, mid.Authenticate(authenticator))
|
||||||
app.Handle("POST", "/v1/projects", p.Create, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
|
app.Handle("POST", "/v1/projects", p.Create, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
|
||||||
app.Handle("GET", "/v1/projects/:id", p.Read, mid.Authenticate(authenticator))
|
app.Handle("GET", "/v1/projects/:id", p.Read, mid.Authenticate(authenticator))
|
||||||
app.Handle("PATCH", "/v1/projects/:id", p.Update, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
|
app.Handle("PATCH", "/v1/projects", p.Update, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
|
||||||
app.Handle("PATCH", "/v1/projects/:id/archive", p.Archive, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
|
app.Handle("PATCH", "/v1/projects/archive", p.Archive, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
|
||||||
app.Handle("DELETE", "/v1/projects/:id", p.Delete, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
|
app.Handle("DELETE", "/v1/projects/:id", p.Delete, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
|
||||||
|
|
||||||
// Register swagger documentation.
|
// Register swagger documentation.
|
||||||
@ -78,3 +75,12 @@ func API(shutdown chan os.Signal, log *log.Logger, masterDB *sqlx.DB, redis *red
|
|||||||
|
|
||||||
return app
|
return app
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Types godoc
|
||||||
|
// @Summary List of types.
|
||||||
|
// @Param data body web.FieldError false "Field Error"
|
||||||
|
// @Param data body web.TimeResponse false "Time Response"
|
||||||
|
// @Param data body web.EnumResponse false "Enum Response"
|
||||||
|
// @Param data body web.EnumOption false "Enum Option"
|
||||||
|
// To support nested types not parsed by swag.
|
||||||
|
func Types() {}
|
||||||
|
@ -2,13 +2,15 @@ package handlers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
"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/auth"
|
"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/platform/web"
|
||||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/signup"
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/signup"
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"net/http"
|
"gopkg.in/go-playground/validator.v9"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Signup represents the Signup API method handler set.
|
// Signup represents the Signup API method handler set.
|
||||||
@ -26,9 +28,9 @@ type Signup struct {
|
|||||||
// @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 200 {object} signup.SignupResponse
|
||||||
// @Header 200 {string} Token "qwerty"
|
// @Failure 400 {object} web.ErrorResponse
|
||||||
// @Failure 400 {object} web.Error
|
// @Failure 403 {object} web.ErrorResponse
|
||||||
// @Failure 403 {object} web.Error
|
// @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 {
|
||||||
v, ok := ctx.Value(web.KeyValues).(*web.Values)
|
v, ok := ctx.Value(web.KeyValues).(*web.Values)
|
||||||
@ -41,7 +43,13 @@ 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 {
|
||||||
return errors.Wrap(err, "")
|
err = errors.WithStack(err)
|
||||||
|
|
||||||
|
_, ok := err.(validator.ValidationErrors)
|
||||||
|
if ok {
|
||||||
|
return web.NewRequestError(err, http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := signup.Signup(ctx, claims, c.MasterDB, req, v.Now)
|
res, err := signup.Signup(ctx, claims, c.MasterDB, req, v.Now)
|
||||||
@ -50,7 +58,12 @@ func (c *Signup) Signup(ctx context.Context, w http.ResponseWriter, r *http.Requ
|
|||||||
case account.ErrForbidden:
|
case account.ErrForbidden:
|
||||||
return web.NewRequestError(err, http.StatusForbidden)
|
return web.NewRequestError(err, http.StatusForbidden)
|
||||||
default:
|
default:
|
||||||
return errors.Wrapf(err, "User: %+v", &req)
|
_, ok := err.(validator.ValidationErrors)
|
||||||
|
if ok {
|
||||||
|
return web.NewRequestError(err, http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.Wrapf(err, "Signup: %+v", &req)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth"
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth"
|
||||||
@ -11,6 +12,7 @@ import (
|
|||||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/user"
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/user"
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"gopkg.in/go-playground/validator.v9"
|
||||||
)
|
)
|
||||||
|
|
||||||
// sessionTtl defines the auth token expiration.
|
// sessionTtl defines the auth token expiration.
|
||||||
@ -24,7 +26,23 @@ type User struct {
|
|||||||
// ADD OTHER STATE LIKE THE LOGGER AND CONFIG HERE.
|
// ADD OTHER STATE LIKE THE LOGGER AND CONFIG HERE.
|
||||||
}
|
}
|
||||||
|
|
||||||
// List returns all the existing users in the system.
|
// Find godoc
|
||||||
|
// @Summary List users
|
||||||
|
// @Description Find returns the existing users in the system.
|
||||||
|
// @Tags user
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security OAuth2Password
|
||||||
|
// @Param where query string false "Filter string, example: name = 'Company Name' and email = 'gabi.may@geeksinthewoods.com'"
|
||||||
|
// @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.UserResponse
|
||||||
|
// @Failure 400 {object} web.ErrorResponse
|
||||||
|
// @Failure 403 {object} web.ErrorResponse
|
||||||
|
// @Failure 500 {object} web.ErrorResponse
|
||||||
|
// @Router /users [get]
|
||||||
func (u *User) Find(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
func (u *User) Find(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||||
claims, ok := ctx.Value(auth.Key).(auth.Claims)
|
claims, ok := ctx.Value(auth.Key).(auth.Claims)
|
||||||
if !ok {
|
if !ok {
|
||||||
@ -32,8 +50,67 @@ func (u *User) Find(ctx context.Context, w http.ResponseWriter, r *http.Request,
|
|||||||
}
|
}
|
||||||
|
|
||||||
var req user.UserFindRequest
|
var req user.UserFindRequest
|
||||||
|
|
||||||
|
// 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.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.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.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.NewRequestError(err, http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
req.IncludedArchived = b
|
||||||
|
}
|
||||||
|
|
||||||
if err := web.Decode(r, &req); err != nil {
|
if err := web.Decode(r, &req); err != nil {
|
||||||
return errors.Wrap(err, "")
|
err = errors.WithStack(err)
|
||||||
|
|
||||||
|
_, ok := err.(validator.ValidationErrors)
|
||||||
|
if ok {
|
||||||
|
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)
|
||||||
@ -41,22 +118,27 @@ func (u *User) Find(ctx context.Context, w http.ResponseWriter, r *http.Request,
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return web.RespondJson(ctx, w, res, http.StatusOK)
|
var resp []*user.UserResponse
|
||||||
|
for _, m := range res {
|
||||||
|
resp = append(resp, m.Response(ctx))
|
||||||
|
}
|
||||||
|
|
||||||
|
return web.RespondJson(ctx, w, resp, http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read godoc
|
// Read godoc
|
||||||
// @Summary Read returns the specified user from the system.
|
// @Summary Get user by ID
|
||||||
// @Description get string by ID
|
// @Description Read returns the specified user from the system.
|
||||||
// @Tags user
|
// @Tags user
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Security OAuth2Password
|
// @Security OAuth2Password
|
||||||
// @Param id path string true "User ID"
|
// @Param id path string true "User ID"
|
||||||
// @Success 200 {object} user.User
|
// @Success 200 {object} user.UserResponse
|
||||||
// @Header 200 {string} Token "qwerty"
|
// @Failure 400 {object} web.ErrorResponse
|
||||||
// @Failure 400 {object} web.Error
|
// @Failure 403 {object} web.ErrorResponse
|
||||||
// @Failure 403 {object} web.Error
|
// @Failure 404 {object} web.ErrorResponse
|
||||||
// @Failure 404 {object} web.Error
|
// @Failure 500 {object} web.ErrorResponse
|
||||||
// @Router /users/{id} [get]
|
// @Router /users/{id} [get]
|
||||||
func (u *User) Read(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
func (u *User) Read(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||||
claims, ok := ctx.Value(auth.Key).(auth.Claims)
|
claims, ok := ctx.Value(auth.Key).(auth.Claims)
|
||||||
@ -83,14 +165,32 @@ func (u *User) Read(ctx context.Context, w http.ResponseWriter, r *http.Request,
|
|||||||
case user.ErrForbidden:
|
case user.ErrForbidden:
|
||||||
return web.NewRequestError(err, http.StatusForbidden)
|
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"])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return web.RespondJson(ctx, w, res, http.StatusOK)
|
return web.RespondJson(ctx, w, res.Response(ctx), http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create inserts a new user into the system.
|
// Create godoc
|
||||||
|
// @Summary Create new user.
|
||||||
|
// @Description Create inserts a new user into the system.
|
||||||
|
// @Tags user
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security OAuth2Password
|
||||||
|
// @Param data body user.UserCreateRequest true "User details"
|
||||||
|
// @Success 200 {object} user.UserResponse
|
||||||
|
// @Failure 400 {object} web.ErrorResponse
|
||||||
|
// @Failure 403 {object} web.ErrorResponse
|
||||||
|
// @Failure 404 {object} web.ErrorResponse
|
||||||
|
// @Failure 500 {object} web.ErrorResponse
|
||||||
|
// @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 {
|
||||||
v, ok := ctx.Value(web.KeyValues).(*web.Values)
|
v, ok := ctx.Value(web.KeyValues).(*web.Values)
|
||||||
if !ok {
|
if !ok {
|
||||||
@ -104,7 +204,13 @@ 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 {
|
||||||
return errors.Wrap(err, "")
|
err = errors.WithStack(err)
|
||||||
|
|
||||||
|
_, ok := err.(validator.ValidationErrors)
|
||||||
|
if ok {
|
||||||
|
return web.NewRequestError(err, http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := user.Create(ctx, claims, u.MasterDB, req, v.Now)
|
res, err := user.Create(ctx, claims, u.MasterDB, req, v.Now)
|
||||||
@ -113,14 +219,32 @@ func (u *User) Create(ctx context.Context, w http.ResponseWriter, r *http.Reques
|
|||||||
case user.ErrForbidden:
|
case user.ErrForbidden:
|
||||||
return web.NewRequestError(err, http.StatusForbidden)
|
return web.NewRequestError(err, http.StatusForbidden)
|
||||||
default:
|
default:
|
||||||
|
_, ok := err.(validator.ValidationErrors)
|
||||||
|
if ok {
|
||||||
|
return web.NewRequestError(err, http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
return errors.Wrapf(err, "User: %+v", &req)
|
return errors.Wrapf(err, "User: %+v", &req)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return web.RespondJson(ctx, w, res, http.StatusCreated)
|
return web.RespondJson(ctx, w, res.Response(ctx), http.StatusCreated)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update updates the specified user in the system.
|
// Read godoc
|
||||||
|
// @Summary Update user by ID
|
||||||
|
// @Description Update updates the specified user in the system.
|
||||||
|
// @Tags user
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security OAuth2Password
|
||||||
|
// @Param data body user.UserUpdateRequest true "Update fields"
|
||||||
|
// @Success 201
|
||||||
|
// @Failure 400 {object} web.ErrorResponse
|
||||||
|
// @Failure 403 {object} web.ErrorResponse
|
||||||
|
// @Failure 404 {object} web.ErrorResponse
|
||||||
|
// @Failure 500 {object} web.ErrorResponse
|
||||||
|
// @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 {
|
||||||
v, ok := ctx.Value(web.KeyValues).(*web.Values)
|
v, ok := ctx.Value(web.KeyValues).(*web.Values)
|
||||||
if !ok {
|
if !ok {
|
||||||
@ -134,9 +258,14 @@ 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 {
|
||||||
return errors.Wrap(err, "")
|
err = errors.WithStack(err)
|
||||||
|
|
||||||
|
_, ok := err.(validator.ValidationErrors)
|
||||||
|
if ok {
|
||||||
|
return web.NewRequestError(err, http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
req.ID = params["id"]
|
|
||||||
|
|
||||||
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 {
|
||||||
@ -148,14 +277,32 @@ func (u *User) Update(ctx context.Context, w http.ResponseWriter, r *http.Reques
|
|||||||
case user.ErrForbidden:
|
case user.ErrForbidden:
|
||||||
return web.NewRequestError(err, http.StatusForbidden)
|
return web.NewRequestError(err, http.StatusForbidden)
|
||||||
default:
|
default:
|
||||||
return errors.Wrapf(err, "Id: %s User: %+v", params["id"], &req)
|
_, ok := err.(validator.ValidationErrors)
|
||||||
|
if ok {
|
||||||
|
return web.NewRequestError(err, http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.Wrapf(err, "Id: %s User: %+v", req.ID, &req)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return web.RespondJson(ctx, w, nil, http.StatusNoContent)
|
return web.RespondJson(ctx, w, nil, http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update updates the password for a specified user in the system.
|
// Read godoc
|
||||||
|
// @Summary Update user password by ID
|
||||||
|
// @Description Update updates the password for a specified user in the system.
|
||||||
|
// @Tags user
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security OAuth2Password
|
||||||
|
// @Param data body user.UserUpdatePasswordRequest true "Update fields"
|
||||||
|
// @Success 201
|
||||||
|
// @Failure 400 {object} web.ErrorResponse
|
||||||
|
// @Failure 403 {object} web.ErrorResponse
|
||||||
|
// @Failure 404 {object} web.ErrorResponse
|
||||||
|
// @Failure 500 {object} web.ErrorResponse
|
||||||
|
// @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 {
|
||||||
v, ok := ctx.Value(web.KeyValues).(*web.Values)
|
v, ok := ctx.Value(web.KeyValues).(*web.Values)
|
||||||
if !ok {
|
if !ok {
|
||||||
@ -169,9 +316,14 @@ 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 {
|
||||||
return errors.Wrap(err, "")
|
err = errors.WithStack(err)
|
||||||
|
|
||||||
|
_, ok := err.(validator.ValidationErrors)
|
||||||
|
if ok {
|
||||||
|
return web.NewRequestError(err, http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
req.ID = params["id"]
|
|
||||||
|
|
||||||
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 {
|
||||||
@ -183,14 +335,32 @@ func (u *User) UpdatePassword(ctx context.Context, w http.ResponseWriter, r *htt
|
|||||||
case user.ErrForbidden:
|
case user.ErrForbidden:
|
||||||
return web.NewRequestError(err, http.StatusForbidden)
|
return web.NewRequestError(err, http.StatusForbidden)
|
||||||
default:
|
default:
|
||||||
return errors.Wrapf(err, "Id: %s User: %+v", params["id"], &req)
|
_, ok := err.(validator.ValidationErrors)
|
||||||
|
if ok {
|
||||||
|
return web.NewRequestError(err, http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.Wrapf(err, "Id: %s User: %+v", req.ID, &req)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return web.RespondJson(ctx, w, nil, http.StatusNoContent)
|
return web.RespondJson(ctx, w, nil, http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Archive soft-deletes the specified user from the system.
|
// Read godoc
|
||||||
|
// @Summary Archive user by ID
|
||||||
|
// @Description Archive soft-deletes the specified user from the system.
|
||||||
|
// @Tags user
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security OAuth2Password
|
||||||
|
// @Param data body user.UserArchiveRequest true "Update fields"
|
||||||
|
// @Success 201
|
||||||
|
// @Failure 400 {object} web.ErrorResponse
|
||||||
|
// @Failure 403 {object} web.ErrorResponse
|
||||||
|
// @Failure 404 {object} web.ErrorResponse
|
||||||
|
// @Failure 500 {object} web.ErrorResponse
|
||||||
|
// @Router /users/archive [patch]
|
||||||
func (u *User) Archive(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
func (u *User) Archive(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)
|
||||||
if !ok {
|
if !ok {
|
||||||
@ -202,7 +372,18 @@ func (u *User) Archive(ctx context.Context, w http.ResponseWriter, r *http.Reque
|
|||||||
return errors.New("claims missing from context")
|
return errors.New("claims missing from context")
|
||||||
}
|
}
|
||||||
|
|
||||||
err := user.Archive(ctx, claims, u.MasterDB, params["id"], v.Now)
|
var req user.UserArchiveRequest
|
||||||
|
if err := web.Decode(r, &req); err != nil {
|
||||||
|
err = errors.WithStack(err)
|
||||||
|
|
||||||
|
_, ok := err.(validator.ValidationErrors)
|
||||||
|
if ok {
|
||||||
|
return web.NewRequestError(err, http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err := user.Archive(ctx, claims, u.MasterDB, req, v.Now)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch err {
|
switch err {
|
||||||
case user.ErrInvalidID:
|
case user.ErrInvalidID:
|
||||||
@ -212,14 +393,32 @@ func (u *User) Archive(ctx context.Context, w http.ResponseWriter, r *http.Reque
|
|||||||
case user.ErrForbidden:
|
case user.ErrForbidden:
|
||||||
return web.NewRequestError(err, http.StatusForbidden)
|
return web.NewRequestError(err, http.StatusForbidden)
|
||||||
default:
|
default:
|
||||||
return errors.Wrapf(err, "Id: %s", params["id"])
|
_, ok := err.(validator.ValidationErrors)
|
||||||
|
if ok {
|
||||||
|
return web.NewRequestError(err, http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.Wrapf(err, "Id: %s", req.ID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return web.RespondJson(ctx, w, nil, http.StatusNoContent)
|
return web.RespondJson(ctx, w, nil, http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete removes the specified user from the system.
|
// Delete godoc
|
||||||
|
// @Summary Delete user by ID
|
||||||
|
// @Description Delete removes the specified user from the system.
|
||||||
|
// @Tags user
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security OAuth2Password
|
||||||
|
// @Param id path string true "User ID"
|
||||||
|
// @Success 201
|
||||||
|
// @Failure 400 {object} web.ErrorResponse
|
||||||
|
// @Failure 403 {object} web.ErrorResponse
|
||||||
|
// @Failure 404 {object} web.ErrorResponse
|
||||||
|
// @Failure 500 {object} web.ErrorResponse
|
||||||
|
// @Router /users/{id} [delete]
|
||||||
func (u *User) Delete(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
func (u *User) Delete(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||||
claims, ok := ctx.Value(auth.Key).(auth.Claims)
|
claims, ok := ctx.Value(auth.Key).(auth.Claims)
|
||||||
if !ok {
|
if !ok {
|
||||||
@ -243,7 +442,20 @@ func (u *User) Delete(ctx context.Context, w http.ResponseWriter, r *http.Reques
|
|||||||
return web.RespondJson(ctx, w, nil, http.StatusNoContent)
|
return web.RespondJson(ctx, w, nil, http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SwitchAccount updates the claims.
|
// SwitchAccount godoc
|
||||||
|
// @Summary Switch account.
|
||||||
|
// @Description SwitchAccount updates the auth claims to a new account.
|
||||||
|
// @Tags user
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security OAuth2Password
|
||||||
|
// @Param account_id path int true "Account ID"
|
||||||
|
// @Success 201
|
||||||
|
// @Failure 400 {object} web.ErrorResponse
|
||||||
|
// @Failure 403 {object} web.ErrorResponse
|
||||||
|
// @Failure 404 {object} web.ErrorResponse
|
||||||
|
// @Failure 500 {object} web.ErrorResponse
|
||||||
|
// @Router /users/switch-account/{account_id} [patch]
|
||||||
func (u *User) SwitchAccount(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
func (u *User) SwitchAccount(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)
|
||||||
if !ok {
|
if !ok {
|
||||||
@ -255,12 +467,17 @@ func (u *User) SwitchAccount(ctx context.Context, w http.ResponseWriter, r *http
|
|||||||
return errors.New("claims missing from context")
|
return errors.New("claims missing from context")
|
||||||
}
|
}
|
||||||
|
|
||||||
tkn, err := user.SwitchAccount(ctx, u.MasterDB, u.TokenGenerator, claims, params["accountId"], 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 {
|
switch err {
|
||||||
case user.ErrAuthenticationFailure:
|
case user.ErrAuthenticationFailure:
|
||||||
return web.NewRequestError(err, http.StatusUnauthorized)
|
return web.NewRequestError(err, http.StatusUnauthorized)
|
||||||
default:
|
default:
|
||||||
|
_, ok := err.(validator.ValidationErrors)
|
||||||
|
if ok {
|
||||||
|
return web.NewRequestError(err, http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
return errors.Wrap(err, "switch account")
|
return errors.Wrap(err, "switch account")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -275,6 +492,7 @@ func (u *User) SwitchAccount(ctx context.Context, w http.ResponseWriter, r *http
|
|||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Security BasicAuth
|
// @Security BasicAuth
|
||||||
|
// @Param scope query string false "Scope" Enums(user, admin)
|
||||||
// @Success 200 {object} user.Token
|
// @Success 200 {object} user.Token
|
||||||
// @Header 200 {string} Token "qwerty"
|
// @Header 200 {string} Token "qwerty"
|
||||||
// @Failure 400 {object} web.Error
|
// @Failure 400 {object} web.Error
|
||||||
@ -293,7 +511,10 @@ func (u *User) Token(ctx context.Context, w http.ResponseWriter, r *http.Request
|
|||||||
return web.NewRequestError(err, http.StatusUnauthorized)
|
return web.NewRequestError(err, http.StatusUnauthorized)
|
||||||
}
|
}
|
||||||
|
|
||||||
tkn, err := user.Authenticate(ctx, u.MasterDB, u.TokenGenerator, email, pass, sessionTtl, v.Now)
|
// Optional to include scope.
|
||||||
|
scope := r.URL.Query().Get("scope")
|
||||||
|
|
||||||
|
tkn, err := user.Authenticate(ctx, u.MasterDB, u.TokenGenerator, email, pass, sessionTtl, v.Now, scope)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch err {
|
switch err {
|
||||||
case user.ErrAuthenticationFailure:
|
case user.ErrAuthenticationFailure:
|
||||||
|
@ -55,9 +55,8 @@ var service = "WEB_API"
|
|||||||
|
|
||||||
// @securitydefinitions.oauth2.password OAuth2Password
|
// @securitydefinitions.oauth2.password OAuth2Password
|
||||||
// @tokenUrl /v1/oauth/token
|
// @tokenUrl /v1/oauth/token
|
||||||
// @scope.read Grants read access
|
// @scope.user Grants basic privileges with role of user.
|
||||||
// @scope.write Grants write access
|
// @scope.admin Grants administrative privileges with role of admin.
|
||||||
// @scope.admin Grants read and write access to administrative information
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|
||||||
|
@ -36,9 +36,9 @@ require (
|
|||||||
github.com/swaggo/swag v1.5.1
|
github.com/swaggo/swag v1.5.1
|
||||||
github.com/tinylib/msgp v1.1.0 // indirect
|
github.com/tinylib/msgp v1.1.0 // indirect
|
||||||
github.com/urfave/cli v1.20.0
|
github.com/urfave/cli v1.20.0
|
||||||
|
github.com/xwb1989/sqlparser v0.0.0-20180606152119-120387863bf2
|
||||||
golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4
|
golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859 // indirect
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859 // indirect
|
||||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
|
|
||||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0 // indirect
|
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0 // indirect
|
||||||
golang.org/x/tools v0.0.0-20190624222133-a101b041ded4 // indirect
|
golang.org/x/tools v0.0.0-20190624222133-a101b041ded4 // indirect
|
||||||
google.golang.org/appengine v1.6.0 // indirect
|
google.golang.org/appengine v1.6.0 // indirect
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
|
||||||
github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||||
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
|
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
|
||||||
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||||
@ -123,6 +122,8 @@ github.com/tinylib/msgp v1.1.0 h1:9fQd+ICuRIu/ue4vxJZu6/LzxN0HwMds2nq/0cFvxHU=
|
|||||||
github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
||||||
github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw=
|
github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw=
|
||||||
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
||||||
|
github.com/xwb1989/sqlparser v0.0.0-20180606152119-120387863bf2 h1:zzrxE1FKn5ryBNl9eKOeqQ58Y/Qpo3Q9QNxKHX5uzzQ=
|
||||||
|
github.com/xwb1989/sqlparser v0.0.0-20180606152119-120387863bf2/go.mod h1:hzfGeIUDq/j97IG+FhNqkowIyEcD88LrW6fyU3K3WqY=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4 h1:ydJNl0ENAG67pFbB+9tfhiL2pYqLhfoaZFw/cjLhY4A=
|
golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4 h1:ydJNl0ENAG67pFbB+9tfhiL2pYqLhfoaZFw/cjLhY4A=
|
||||||
@ -131,16 +132,12 @@ golang.org/x/net v0.0.0-20180724234803-3673e40ba225 h1:kNX+jCowfMYzvlSvJu5pQWEmy
|
|||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
@ -159,7 +156,6 @@ golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgw
|
|||||||
golang.org/x/tools v0.0.0-20190624222133-a101b041ded4 h1:1mMox4TgefDwqluYCv677yNXwlfTkija4owZve/jr78=
|
golang.org/x/tools v0.0.0-20190624222133-a101b041ded4 h1:1mMox4TgefDwqluYCv677yNXwlfTkija4owZve/jr78=
|
||||||
golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||||
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
|
||||||
google.golang.org/appengine v1.6.0 h1:Tfd7cKwKbFRsI8RMAD3oqqw7JPFRrvFlOsfbgVkjOOw=
|
google.golang.org/appengine v1.6.0 h1:Tfd7cKwKbFRsI8RMAD3oqqw7JPFRrvFlOsfbgVkjOOw=
|
||||||
google.golang.org/appengine v1.6.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
google.golang.org/appengine v1.6.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
gopkg.in/DataDog/dd-trace-go.v1 v1.15.0 h1:2LhklnAJsRSelbnBrrE5QuRleRDkmOh2JWxOtIX6yec=
|
gopkg.in/DataDog/dd-trace-go.v1 v1.15.0 h1:2LhklnAJsRSelbnBrrE5QuRleRDkmOh2JWxOtIX6yec=
|
||||||
|
@ -481,17 +481,18 @@ func Update(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req Accoun
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Archive soft deleted the account from the database.
|
// Archive soft deleted the account by ID from the database.
|
||||||
func Archive(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, accountID string, now time.Time) error {
|
func ArchiveById(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, accountID string, now time.Time) error {
|
||||||
span, ctx := tracer.StartSpanFromContext(ctx, "internal.account.Archive")
|
req := AccountArchiveRequest{
|
||||||
defer span.Finish()
|
|
||||||
|
|
||||||
// Defines the struct to apply validation
|
|
||||||
req := struct {
|
|
||||||
ID string `validate:"required,uuid"`
|
|
||||||
}{
|
|
||||||
ID: accountID,
|
ID: accountID,
|
||||||
}
|
}
|
||||||
|
return Archive(ctx, claims, dbConn, req, now)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Archive soft deleted the account from the database.
|
||||||
|
func Archive(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req AccountArchiveRequest, now time.Time) error {
|
||||||
|
span, ctx := tracer.StartSpanFromContext(ctx, "internal.account.Archive")
|
||||||
|
defer span.Finish()
|
||||||
|
|
||||||
// Validate the request.
|
// Validate the request.
|
||||||
err := validator.New().Struct(req)
|
err := validator.New().Struct(req)
|
||||||
|
@ -792,7 +792,7 @@ func TestCrud(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Archive (soft-delete) the account.
|
// Archive (soft-delete) the account.
|
||||||
err = Archive(ctx, tt.claims(account, userId), test.MasterDB, account.ID, now)
|
err = ArchiveById(ctx, tt.claims(account, userId), test.MasterDB, account.ID, now)
|
||||||
if err != nil && errors.Cause(err) != tt.updateErr {
|
if err != nil && errors.Cause(err) != tt.updateErr {
|
||||||
t.Logf("\t\tGot : %+v", err)
|
t.Logf("\t\tGot : %+v", err)
|
||||||
t.Logf("\t\tWant: %+v", tt.updateErr)
|
t.Logf("\t\tWant: %+v", tt.updateErr)
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
package account
|
package account
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"database/sql/driver"
|
"database/sql/driver"
|
||||||
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/lib/pq"
|
"github.com/lib/pq"
|
||||||
@ -12,6 +14,25 @@ import (
|
|||||||
|
|
||||||
// Account represents someone with access to our system.
|
// Account represents someone with access to our system.
|
||||||
type Account struct {
|
type Account struct {
|
||||||
|
ID string `json:"id" validate:"required,uuid" example:"c4653bf9-5978-48b7-89c5-95704aebb7e2"`
|
||||||
|
Name string `json:"name" validate:"required,unique" example:"Company Name"`
|
||||||
|
Address1 string `json:"address1" validate:"required" example:"221 Tatitlek Ave"`
|
||||||
|
Address2 string `json:"address2" validate:"omitempty" example:"Box #1832"`
|
||||||
|
City string `json:"city" validate:"required" example:"Valdez"`
|
||||||
|
Region string `json:"region" validate:"required" example:"AK"`
|
||||||
|
Country string `json:"country" validate:"required" example:"USA"`
|
||||||
|
Zipcode string `json:"zipcode" validate:"required" example:"99686"`
|
||||||
|
Status AccountStatus `json:"status" validate:"omitempty,oneof=active pending disabled" swaggertype:"string" enums:"active,pending,disabled" example:"active"`
|
||||||
|
Timezone string `json:"timezone" validate:"omitempty" example:"America/Anchorage"`
|
||||||
|
SignupUserID *sql.NullString `json:"signup_user_id,omitempty" validate:"omitempty,uuid" swaggertype:"string" example:"d69bdef7-173f-4d29-b52c-3edc60baf6a2"`
|
||||||
|
BillingUserID *sql.NullString `json:"billing_user_id,omitempty" validate:"omitempty,uuid" swaggertype:"string" example:"d69bdef7-173f-4d29-b52c-3edc60baf6a2"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
ArchivedAt *pq.NullTime `json:"archived_at,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AccountResponse represents someone with access to our system that is returned for display.
|
||||||
|
type AccountResponse struct {
|
||||||
ID string `json:"id" example:"c4653bf9-5978-48b7-89c5-95704aebb7e2"`
|
ID string `json:"id" example:"c4653bf9-5978-48b7-89c5-95704aebb7e2"`
|
||||||
Name string `json:"name" example:"Company Name"`
|
Name string `json:"name" example:"Company Name"`
|
||||||
Address1 string `json:"address1" example:"221 Tatitlek Ave"`
|
Address1 string `json:"address1" example:"221 Tatitlek Ave"`
|
||||||
@ -20,13 +41,50 @@ type Account struct {
|
|||||||
Region string `json:"region" example:"AK"`
|
Region string `json:"region" example:"AK"`
|
||||||
Country string `json:"country" example:"USA"`
|
Country string `json:"country" example:"USA"`
|
||||||
Zipcode string `json:"zipcode" example:"99686"`
|
Zipcode string `json:"zipcode" example:"99686"`
|
||||||
Status AccountStatus `json:"status" swaggertype:"string" example:"active"`
|
Status web.EnumResponse `json:"status"` // Status is enum with values [active, pending, disabled].
|
||||||
Timezone string `json:"timezone" example:"America/Anchorage"`
|
Timezone string `json:"timezone" example:"America/Anchorage"`
|
||||||
SignupUserID *sql.NullString `json:"signup_user_id,omitempty" swaggertype:"string"`
|
SignupUserID *string `json:"signup_user_id,omitempty" swaggertype:"string" example:"d69bdef7-173f-4d29-b52c-3edc60baf6a2"`
|
||||||
BillingUserID *sql.NullString `json:"billing_user_id,omitempty" swaggertype:"string"`
|
BillingUserID *string `json:"billing_user_id,omitempty" swaggertype:"string" example:"d69bdef7-173f-4d29-b52c-3edc60baf6a2"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt web.TimeResponse `json:"created_at"` // CreatedAt contains multiple format options for display.
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt web.TimeResponse `json:"updated_at"` // UpdatedAt contains multiple format options for display.
|
||||||
ArchivedAt *pq.NullTime `json:"archived_at,omitempty"`
|
ArchivedAt *web.TimeResponse `json:"archived_at,omitempty"` // ArchivedAt contains multiple format options for display.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response transforms Account and AccountResponse that is used for display.
|
||||||
|
// Additional filtering by context values or translations could be applied.
|
||||||
|
func (m *Account) Response(ctx context.Context) *AccountResponse {
|
||||||
|
if m == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
r := &AccountResponse{
|
||||||
|
ID: m.ID,
|
||||||
|
Name: m.Name,
|
||||||
|
Address1: m.Address1,
|
||||||
|
Address2: m.Address2,
|
||||||
|
City: m.City,
|
||||||
|
Region: m.Region,
|
||||||
|
Country: m.Country,
|
||||||
|
Zipcode: m.Zipcode,
|
||||||
|
Timezone: m.Timezone,
|
||||||
|
Status: web.NewEnumResponse(ctx, m.Status, AccountStatus_Values),
|
||||||
|
CreatedAt: web.NewTimeResponse(ctx, m.CreatedAt),
|
||||||
|
UpdatedAt: web.NewTimeResponse(ctx, m.UpdatedAt),
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.SignupUserID != nil {
|
||||||
|
r.SignupUserID = &m.SignupUserID.String
|
||||||
|
}
|
||||||
|
if m.BillingUserID != nil {
|
||||||
|
r.BillingUserID = &m.BillingUserID.String
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.ArchivedAt != nil && !m.ArchivedAt.Time.IsZero() {
|
||||||
|
at := web.NewTimeResponse(ctx, m.ArchivedAt.Time)
|
||||||
|
r.ArchivedAt = &at
|
||||||
|
}
|
||||||
|
|
||||||
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
// AccountCreateRequest contains information needed to create a new Account.
|
// AccountCreateRequest contains information needed to create a new Account.
|
||||||
@ -40,8 +98,8 @@ type AccountCreateRequest struct {
|
|||||||
Zipcode string `json:"zipcode" validate:"required" example:"99686"`
|
Zipcode string `json:"zipcode" validate:"required" example:"99686"`
|
||||||
Status *AccountStatus `json:"status,omitempty" validate:"omitempty,oneof=active pending disabled" swaggertype:"string" enums:"active,pending,disabled" example:"active"`
|
Status *AccountStatus `json:"status,omitempty" validate:"omitempty,oneof=active pending disabled" swaggertype:"string" enums:"active,pending,disabled" example:"active"`
|
||||||
Timezone *string `json:"timezone,omitempty" validate:"omitempty" example:"America/Anchorage"`
|
Timezone *string `json:"timezone,omitempty" validate:"omitempty" example:"America/Anchorage"`
|
||||||
SignupUserID *string `json:"signup_user_id,omitempty" validate:"omitempty,uuid" swaggertype:"string"`
|
SignupUserID *string `json:"signup_user_id,omitempty" validate:"omitempty,uuid" swaggertype:"string" example:"d69bdef7-173f-4d29-b52c-3edc60baf6a2"`
|
||||||
BillingUserID *string `json:"billing_user_id,omitempty" validate:"omitempty,uuid" swaggertype:"string"`
|
BillingUserID *string `json:"billing_user_id,omitempty" validate:"omitempty,uuid" swaggertype:"string" example:"d69bdef7-173f-4d29-b52c-3edc60baf6a2"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// AccountUpdateRequest defines what information may be provided to modify an existing
|
// AccountUpdateRequest defines what information may be provided to modify an existing
|
||||||
@ -51,29 +109,35 @@ type AccountCreateRequest struct {
|
|||||||
// we do not want to use pointers to basic types but we make exceptions around
|
// we do not want to use pointers to basic types but we make exceptions around
|
||||||
// marshalling/unmarshalling.
|
// marshalling/unmarshalling.
|
||||||
type AccountUpdateRequest struct {
|
type AccountUpdateRequest struct {
|
||||||
ID string `json:"id" validate:"required,uuid"`
|
ID string `json:"id" validate:"required,uuid" example:"c4653bf9-5978-48b7-89c5-95704aebb7e2"`
|
||||||
Name *string `json:"name,omitempty" validate:"omitempty,unique"`
|
Name *string `json:"name,omitempty" validate:"omitempty,unique" example:"Company Name"`
|
||||||
Address1 *string `json:"address1,omitempty" validate:"omitempty"`
|
Address1 *string `json:"address1,omitempty" validate:"omitempty" example:"221 Tatitlek Ave"`
|
||||||
Address2 *string `json:"address2,omitempty" validate:"omitempty"`
|
Address2 *string `json:"address2,omitempty" validate:"omitempty" example:"Box #1832"`
|
||||||
City *string `json:"city,omitempty" validate:"omitempty"`
|
City *string `json:"city,omitempty" validate:"omitempty" example:"Valdez"`
|
||||||
Region *string `json:"region,omitempty" validate:"omitempty"`
|
Region *string `json:"region,omitempty" validate:"omitempty" example:"AK"`
|
||||||
Country *string `json:"country,omitempty" validate:"omitempty"`
|
Country *string `json:"country,omitempty" validate:"omitempty" example:"USA"`
|
||||||
Zipcode *string `json:"zipcode,omitempty" validate:"omitempty"`
|
Zipcode *string `json:"zipcode,omitempty" validate:"omitempty" example:"99686"`
|
||||||
Status *AccountStatus `json:"status,omitempty" validate:"omitempty,oneof=active pending disabled" swaggertype:"string" enums:"active,pending,disabled"`
|
Status *AccountStatus `json:"status,omitempty" validate:"omitempty,oneof=active pending disabled" swaggertype:"string" enums:"active,pending,disabled" example:"disabled"`
|
||||||
Timezone *string `json:"timezone,omitempty" validate:"omitempty"`
|
Timezone *string `json:"timezone,omitempty" validate:"omitempty" example:"America/Anchorage"`
|
||||||
SignupUserID *string `json:"signup_user_id,omitempty" validate:"omitempty,uuid" swaggertype:"string"`
|
SignupUserID *string `json:"signup_user_id,omitempty" validate:"omitempty,uuid" swaggertype:"string" example:"d69bdef7-173f-4d29-b52c-3edc60baf6a2"`
|
||||||
BillingUserID *string `json:"billing_user_id,omitempty" validate:"omitempty,uuid" swaggertype:"string"`
|
BillingUserID *string `json:"billing_user_id,omitempty" validate:"omitempty,uuid" swaggertype:"string" example:"d69bdef7-173f-4d29-b52c-3edc60baf6a2"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AccountArchiveRequest defines the information needed to archive an account. This will archive (soft-delete) the
|
||||||
|
// existing database entry.
|
||||||
|
type AccountArchiveRequest struct {
|
||||||
|
ID string `json:"id" validate:"required,uuid" example:"c4653bf9-5978-48b7-89c5-95704aebb7e2"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// AccountFindRequest defines the possible options to search for accounts. By default
|
// AccountFindRequest defines the possible options to search for accounts. By default
|
||||||
// archived accounts will be excluded from response.
|
// archived accounts will be excluded from response.
|
||||||
type AccountFindRequest struct {
|
type AccountFindRequest struct {
|
||||||
Where *string `json:"where"`
|
Where *string `json:"where" example:"name = ? and status = ?"`
|
||||||
Args []interface{} `json:"args" swaggertype:"array,string"`
|
Args []interface{} `json:"args" swaggertype:"array,string" example:"Company Name,active"`
|
||||||
Order []string `json:"order"`
|
Order []string `json:"order" example:"created_at desc"`
|
||||||
Limit *uint `json:"limit"`
|
Limit *uint `json:"limit" example:"10"`
|
||||||
Offset *uint `json:"offset"`
|
Offset *uint `json:"offset" example:"20"`
|
||||||
IncludedArchived bool `json:"included-archived"`
|
IncludedArchived bool `json:"included-archived" example:"false"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// AccountStatus represents the status of an account.
|
// AccountStatus represents the status of an account.
|
||||||
|
@ -24,13 +24,15 @@ const Key ctxKey = 1
|
|||||||
type Claims struct {
|
type Claims struct {
|
||||||
AccountIds []string `json:"accounts"`
|
AccountIds []string `json:"accounts"`
|
||||||
Roles []string `json:"roles"`
|
Roles []string `json:"roles"`
|
||||||
|
Timezone string `json:"timezone"`
|
||||||
|
tz *time.Location
|
||||||
jwt.StandardClaims
|
jwt.StandardClaims
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewClaims constructs a Claims value for the identified user. The Claims
|
// NewClaims constructs a Claims value for the identified user. The Claims
|
||||||
// expire within a specified duration of the provided time. Additional fields
|
// expire within a specified duration of the provided time. Additional fields
|
||||||
// of the Claims can be set after calling NewClaims is desired.
|
// of the Claims can be set after calling NewClaims is desired.
|
||||||
func NewClaims(userId, accountId string, accountIds []string, roles []string, now time.Time, expires time.Duration) Claims {
|
func NewClaims(userId, accountId string, accountIds []string, roles []string, userTimezone *time.Location, now time.Time, expires time.Duration) Claims {
|
||||||
c := Claims{
|
c := Claims{
|
||||||
AccountIds: accountIds,
|
AccountIds: accountIds,
|
||||||
Roles: roles,
|
Roles: roles,
|
||||||
@ -42,6 +44,10 @@ func NewClaims(userId, accountId string, accountIds []string, roles []string, no
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if userTimezone != nil {
|
||||||
|
c.Timezone = userTimezone.String()
|
||||||
|
}
|
||||||
|
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,3 +77,10 @@ func (c Claims) HasRole(roles ...string) bool {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c Claims) TimeLocation() *time.Location {
|
||||||
|
if c.tz == nil && c.Timezone != "" {
|
||||||
|
c.tz, _ = time.LoadLocation(c.Timezone)
|
||||||
|
}
|
||||||
|
return c.tz
|
||||||
|
}
|
||||||
|
16
example-project/internal/platform/session/session.go
Normal file
16
example-project/internal/platform/session/session.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package session
|
||||||
|
|
||||||
|
import (
|
||||||
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ctxKey represents the type of value for the context key.
|
||||||
|
type ctxKey int
|
||||||
|
|
||||||
|
// Key is used to store/retrieve a Claims value from a context.Context.
|
||||||
|
const Key ctxKey = 1
|
||||||
|
|
||||||
|
// Session represents a user with authentication.
|
||||||
|
type Session struct {
|
||||||
|
Claims auth.Claims `json:"claims"`
|
||||||
|
}
|
99
example-project/internal/platform/web/models.go
Normal file
99
example-project/internal/platform/web/models.go
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
package web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth"
|
||||||
|
"github.com/dustin/go-humanize"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const DatetimeFormatLocal = "Mon Jan _2 3:04PM"
|
||||||
|
const DateFormatLocal = "Mon Jan _2"
|
||||||
|
|
||||||
|
// TimeResponse is a response friendly format for displaying the value of a time.
|
||||||
|
type TimeResponse struct {
|
||||||
|
Value time.Time `json:"value" example:"2019-06-25T03:00:53.284-08:00"`
|
||||||
|
ValueUTC time.Time `json:"value_utc" example:"2019-06-25T11:00:53.284Z"`
|
||||||
|
Date string `json:"date" example:"2019-06-25"`
|
||||||
|
Time string `json:"time" example:"03:00:53"`
|
||||||
|
Kitchen string `json:"kitchen" example:"3:00AM"`
|
||||||
|
RFC1123 string `json:"rfc1123" example:"Tue, 25 Jun 2019 03:00:53 AKDT"`
|
||||||
|
Local string `json:"local" example:"Tue Jun 25 3:00AM"`
|
||||||
|
LocalDate string `json:"local_date" example:"Tue Jun 25"`
|
||||||
|
NowTime string `json:"now_time" example:"5 hours ago"`
|
||||||
|
NowRelTime string `json:"now_rel_time" example:"15 hours from now"`
|
||||||
|
Timezone string `json:"timezone" example:"America/Anchorage"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTimeResponse parses the time to the timezone location set in context and
|
||||||
|
// returns the display friendly format as TimeResponse.
|
||||||
|
func NewTimeResponse(ctx context.Context, t time.Time) TimeResponse {
|
||||||
|
|
||||||
|
// If the context has claims, check to see if timezone is set for the current user and
|
||||||
|
// then format the input time in that timezone if set.
|
||||||
|
claims, ok := ctx.Value(auth.Key).(auth.Claims)
|
||||||
|
if ok && claims.TimeLocation() != nil {
|
||||||
|
t = t.In(claims.TimeLocation())
|
||||||
|
}
|
||||||
|
|
||||||
|
tr := TimeResponse{
|
||||||
|
Value: t,
|
||||||
|
ValueUTC: t.UTC(),
|
||||||
|
Date: t.Format("2006-01-02"),
|
||||||
|
Time: t.Format("15:04:05"),
|
||||||
|
Kitchen: t.Format(time.Kitchen),
|
||||||
|
RFC1123: t.Format(time.RFC1123),
|
||||||
|
Local: t.Format(DatetimeFormatLocal),
|
||||||
|
LocalDate: t.Format(DateFormatLocal),
|
||||||
|
NowTime: humanize.Time(t.UTC()),
|
||||||
|
NowRelTime: humanize.RelTime(time.Now().UTC(), t.UTC(), "ago", "from now"),
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.Location() != nil {
|
||||||
|
tr.Timezone = t.Location().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
return tr
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnumOption represents a single value of an enum option.
|
||||||
|
type EnumOption struct {
|
||||||
|
Value string `json:"value" example:"active_etc"`
|
||||||
|
Title string `json:"title" example:"Active Etc"`
|
||||||
|
Selected bool `json:"selected" example:"true"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnumResponse is a response friendly format for displaying an enum value that
|
||||||
|
// includes a list of all possible values.
|
||||||
|
type EnumResponse struct {
|
||||||
|
Value string `json:"value" example:"active_etc"`
|
||||||
|
Title string `json:"title" example:"Active Etc"`
|
||||||
|
Options []EnumOption `json:"options,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewEnumResponse returns a display friendly format for a enum field.
|
||||||
|
func NewEnumResponse(ctx context.Context, value interface{}, options ...interface{}) EnumResponse {
|
||||||
|
er := EnumResponse{
|
||||||
|
Value: fmt.Sprintf("%s", value),
|
||||||
|
Title: EnumValueTitle(fmt.Sprintf("%s", value)),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, opt := range options {
|
||||||
|
optStr := fmt.Sprintf("%s", opt)
|
||||||
|
er.Options = append(er.Options, EnumOption{
|
||||||
|
Value: optStr,
|
||||||
|
Title: EnumValueTitle(optStr),
|
||||||
|
Selected: (value == opt),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return er
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnumValueTitle formats a string value for display.
|
||||||
|
func EnumValueTitle(v string) string {
|
||||||
|
v = strings.Replace(v, "_", " ", -1)
|
||||||
|
return strings.Title(v)
|
||||||
|
}
|
@ -2,7 +2,6 @@ package web
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
@ -10,6 +9,9 @@ import (
|
|||||||
"github.com/go-playground/locales/en"
|
"github.com/go-playground/locales/en"
|
||||||
ut "github.com/go-playground/universal-translator"
|
ut "github.com/go-playground/universal-translator"
|
||||||
"github.com/gorilla/schema"
|
"github.com/gorilla/schema"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/xwb1989/sqlparser"
|
||||||
|
"github.com/xwb1989/sqlparser/dependency/querypb"
|
||||||
"gopkg.in/go-playground/validator.v9"
|
"gopkg.in/go-playground/validator.v9"
|
||||||
en_translations "gopkg.in/go-playground/validator.v9/translations/en"
|
en_translations "gopkg.in/go-playground/validator.v9/translations/en"
|
||||||
)
|
)
|
||||||
@ -97,3 +99,43 @@ func Decode(r *http.Request, val interface{}) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExtractWhereArgs extracts the sql args from where. This allows requests to accept sql queries for filters and
|
||||||
|
// then replaces the raw values with placeholders. The resulting query will then be executed with bind vars.
|
||||||
|
func ExtractWhereArgs(where string) (string, []interface{}, error) {
|
||||||
|
// Create a full select sql query.
|
||||||
|
query := "select `t` from test where " + where
|
||||||
|
|
||||||
|
// Parse the query.
|
||||||
|
stmt, err := sqlparser.Parse(query)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, errors.WithMessagef(err, "Failed to parse query - %s", where)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize changes the query statement to use bind values, and updates the bind vars to those values. The
|
||||||
|
// supplied prefix is used to generate the bind var names.
|
||||||
|
bindVars := make(map[string]*querypb.BindVariable)
|
||||||
|
sqlparser.Normalize(stmt, bindVars, "redacted")
|
||||||
|
|
||||||
|
// Loop through all the bind vars and append to the response args list.
|
||||||
|
var vals []interface{}
|
||||||
|
for _, bv := range bindVars {
|
||||||
|
if bv.Values != nil {
|
||||||
|
var l []interface{}
|
||||||
|
for _, v := range bv.Values {
|
||||||
|
l = append(l, string(v.Value))
|
||||||
|
}
|
||||||
|
vals = append(vals, l)
|
||||||
|
} else {
|
||||||
|
vals = append(vals, string(bv.Value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the original query to include the redacted values.
|
||||||
|
query = sqlparser.String(stmt)
|
||||||
|
|
||||||
|
// Parse out the updated where.
|
||||||
|
where = strings.Split(query, " where ")[1]
|
||||||
|
|
||||||
|
return where, vals, nil
|
||||||
|
}
|
||||||
|
64
example-project/internal/platform/web/request_test.go
Normal file
64
example-project/internal/platform/web/request_test.go
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
package web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestExtractWhereArgs(t *testing.T) {
|
||||||
|
|
||||||
|
var queryTests = []struct {
|
||||||
|
where string
|
||||||
|
redacted string
|
||||||
|
args []interface{}
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"name = 'xxxx' or name = :test",
|
||||||
|
"name = :redacted1 or name = :test",
|
||||||
|
[]interface{}{"xxxx"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name = 'xxxx' or name is null",
|
||||||
|
"name = :redacted1 or name is null",
|
||||||
|
[]interface{}{"xxxx"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name = 'xxxx' or name in ('yyyy', 'zzzz')",
|
||||||
|
"name = :redacted1 or name in ::redacted2",
|
||||||
|
[]interface{}{"xxxx", []interface{}{"yyyy", "zzzz"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id = 3232 or id in (2323, 3239, 483484)",
|
||||||
|
"id = :redacted1 or id in ::redacted2",
|
||||||
|
[]interface{}{"3232", []interface{}{"2323", "3239", "483484"}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Log("Given the need to ensure values are correctly extracted from a where query string.")
|
||||||
|
{
|
||||||
|
for i, tt := range queryTests {
|
||||||
|
t.Logf("\tTest: %d\tWhen running test: #%d", i, i)
|
||||||
|
{
|
||||||
|
res, args, err := ExtractWhereArgs(tt.where)
|
||||||
|
if err != nil {
|
||||||
|
t.Log("\t\tGot :", err)
|
||||||
|
t.Fatalf("\t\tExtract failed.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if res != tt.redacted {
|
||||||
|
t.Logf("\t\tGot : %+v", res)
|
||||||
|
t.Logf("\t\tWant: %+v", tt.redacted)
|
||||||
|
t.Fatalf("\t\tResulting where does not match expected.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff := cmp.Diff(tt.args, args); diff != "" {
|
||||||
|
t.Logf("\t\tGot : %+v", args)
|
||||||
|
t.Logf("\t\tWant: %+v", tt.args)
|
||||||
|
t.Fatalf("\t\tResulting args does not match expected. Diff:\n%s", diff)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("\t\tOk.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,9 @@
|
|||||||
package project
|
package project
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"database/sql/driver"
|
"database/sql/driver"
|
||||||
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web"
|
||||||
"github.com/lib/pq"
|
"github.com/lib/pq"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gopkg.in/go-playground/validator.v9"
|
"gopkg.in/go-playground/validator.v9"
|
||||||
@ -12,18 +14,53 @@ import (
|
|||||||
type Project struct {
|
type Project struct {
|
||||||
ID string `json:"id" validate:"required,uuid" example:"985f1746-1d9f-459f-a2d9-fc53ece5ae86"`
|
ID string `json:"id" validate:"required,uuid" example:"985f1746-1d9f-459f-a2d9-fc53ece5ae86"`
|
||||||
AccountID string `json:"account_id" validate:"required,uuid" truss:"api-create"`
|
AccountID string `json:"account_id" validate:"required,uuid" truss:"api-create"`
|
||||||
Name string `json:"name" validate:"required"`
|
Name string `json:"name" validate:"required" example:"Rocket Launch"`
|
||||||
Status ProjectStatus `json:"status" validate:"omitempty,oneof=active disabled" enums:"active,disabled" swaggertype:"string"`
|
Status ProjectStatus `json:"status" validate:"omitempty,oneof=active disabled" enums:"active,disabled" swaggertype:"string" example:"active"`
|
||||||
CreatedAt time.Time `json:"created_at" truss:"api-read"`
|
CreatedAt time.Time `json:"created_at" truss:"api-read"`
|
||||||
UpdatedAt time.Time `json:"updated_at" truss:"api-read"`
|
UpdatedAt time.Time `json:"updated_at" truss:"api-read"`
|
||||||
ArchivedAt *pq.NullTime `json:"archived_at,omitempty" truss:"api-hide"`
|
ArchivedAt *pq.NullTime `json:"archived_at,omitempty" truss:"api-hide"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ProjectResponse represents a workflow that is returned for display.
|
||||||
|
type ProjectResponse struct {
|
||||||
|
ID string `json:"id" validate:"required,uuid" example:"985f1746-1d9f-459f-a2d9-fc53ece5ae86"`
|
||||||
|
AccountID string `json:"account_id" validate:"required,uuid" truss:"api-create" example:"c4653bf9-5978-48b7-89c5-95704aebb7e2"`
|
||||||
|
Name string `json:"name" validate:"required" example:"Rocket Launch"`
|
||||||
|
Status web.EnumResponse `json:"status"` // Status is enum with values [active, disabled].
|
||||||
|
CreatedAt web.TimeResponse `json:"created_at"` // CreatedAt contains multiple format options for display.
|
||||||
|
UpdatedAt web.TimeResponse `json:"updated_at"` // UpdatedAt contains multiple format options for display.
|
||||||
|
ArchivedAt *web.TimeResponse `json:"archived_at,omitempty"` // ArchivedAt contains multiple format options for display.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response transforms Project and ProjectResponse that is used for display.
|
||||||
|
// Additional filtering by context values or translations could be applied.
|
||||||
|
func (m *Project) Response(ctx context.Context) *ProjectResponse {
|
||||||
|
if m == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
r := &ProjectResponse{
|
||||||
|
ID: m.ID,
|
||||||
|
AccountID: m.AccountID,
|
||||||
|
Name: m.Name,
|
||||||
|
Status: web.NewEnumResponse(ctx, m.Status, ProjectStatus_Values),
|
||||||
|
CreatedAt: web.NewTimeResponse(ctx, m.CreatedAt),
|
||||||
|
UpdatedAt: web.NewTimeResponse(ctx, m.UpdatedAt),
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.ArchivedAt != nil && !m.ArchivedAt.Time.IsZero() {
|
||||||
|
at := web.NewTimeResponse(ctx, m.ArchivedAt.Time)
|
||||||
|
r.ArchivedAt = &at
|
||||||
|
}
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
// ProjectCreateRequest contains information needed to create a new Project.
|
// ProjectCreateRequest contains information needed to create a new Project.
|
||||||
type ProjectCreateRequest struct {
|
type ProjectCreateRequest struct {
|
||||||
AccountID string `json:"account_id" validate:"required,uuid"`
|
AccountID string `json:"account_id" validate:"required,uuid" example:"c4653bf9-5978-48b7-89c5-95704aebb7e2"`
|
||||||
Name string `json:"name" validate:"required"`
|
Name string `json:"name" validate:"required" example:"Rocket Launch"`
|
||||||
Status *ProjectStatus `json:"status,omitempty" validate:"omitempty,oneof=active disabled" enums:"active,disabled" swaggertype:"string"`
|
Status *ProjectStatus `json:"status,omitempty" validate:"omitempty,oneof=active disabled" enums:"active,disabled" swaggertype:"string" example:"active"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProjectUpdateRequest defines what information may be provided to modify an existing
|
// ProjectUpdateRequest defines what information may be provided to modify an existing
|
||||||
@ -31,20 +68,26 @@ type ProjectCreateRequest struct {
|
|||||||
// changed. It uses pointer fields so we can differentiate between a field that
|
// changed. It uses pointer fields so we can differentiate between a field that
|
||||||
// was not provided and a field that was provided as explicitly blank.
|
// was not provided and a field that was provided as explicitly blank.
|
||||||
type ProjectUpdateRequest struct {
|
type ProjectUpdateRequest struct {
|
||||||
ID string `json:"id" validate:"required,uuid"`
|
ID string `json:"id" validate:"required,uuid" example:"985f1746-1d9f-459f-a2d9-fc53ece5ae86"`
|
||||||
Name *string `json:"name,omitempty" validate:"omitempty"`
|
Name *string `json:"name,omitempty" validate:"omitempty" example:"Rocket Launch to Moon"`
|
||||||
Status *ProjectStatus `json:"status,omitempty" validate:"omitempty,oneof=active disabled" enums:"active,disabled" swaggertype:"string"`
|
Status *ProjectStatus `json:"status,omitempty" validate:"omitempty,oneof=active disabled" enums:"active,disabled" swaggertype:"string" example:"disabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProjectArchiveRequest defines the information needed to archive a project. This will archive (soft-delete) the
|
||||||
|
// existing database entry.
|
||||||
|
type ProjectArchiveRequest struct {
|
||||||
|
ID string `json:"id" validate:"required,uuid" example:"985f1746-1d9f-459f-a2d9-fc53ece5ae86"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProjectFindRequest defines the possible options to search for projects. By default
|
// ProjectFindRequest defines the possible options to search for projects. By default
|
||||||
// archived project will be excluded from response.
|
// archived project will be excluded from response.
|
||||||
type ProjectFindRequest struct {
|
type ProjectFindRequest struct {
|
||||||
Where *string `json:"where"`
|
Where *string `json:"where" example:"name = ? and status = ?"`
|
||||||
Args []interface{} `json:"args" swaggertype:"array,string"`
|
Args []interface{} `json:"args" swaggertype:"array,string" example:"Moon Launch,active"`
|
||||||
Order []string `json:"order"`
|
Order []string `json:"order" example:"created_at desc"`
|
||||||
Limit *uint `json:"limit"`
|
Limit *uint `json:"limit" example:"10"`
|
||||||
Offset *uint `json:"offset"`
|
Offset *uint `json:"offset" example:"20"`
|
||||||
IncludedArchived bool `json:"included-archived"`
|
IncludedArchived bool `json:"included-archived" example:"false"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProjectStatus represents the status of project.
|
// ProjectStatus represents the status of project.
|
||||||
|
@ -358,14 +358,19 @@ func Update(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req Projec
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Archive soft deleted the project by ID from the database.
|
||||||
|
func ArchiveById(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, id string, now time.Time) error {
|
||||||
|
req := ProjectArchiveRequest{
|
||||||
|
ID: id,
|
||||||
|
}
|
||||||
|
return Archive(ctx, claims, dbConn, req, now)
|
||||||
|
}
|
||||||
|
|
||||||
// Archive soft deleted the project from the database.
|
// Archive soft deleted the project from the database.
|
||||||
func Archive(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, id string, now time.Time) error {
|
func Archive(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req ProjectArchiveRequest, now time.Time) error {
|
||||||
span, ctx := tracer.StartSpanFromContext(ctx, "internal.project.Archive")
|
span, ctx := tracer.StartSpanFromContext(ctx, "internal.project.Archive")
|
||||||
defer span.Finish()
|
defer span.Finish()
|
||||||
// Defines the struct to apply validation
|
|
||||||
req := struct {
|
|
||||||
ID string `validate:"required,uuid"`
|
|
||||||
}{}
|
|
||||||
// Validate the request.
|
// Validate the request.
|
||||||
err := validator.New().Struct(req)
|
err := validator.New().Struct(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -3,7 +3,9 @@ package user
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
|
"database/sql"
|
||||||
"github.com/dgrijalva/jwt-go"
|
"github.com/dgrijalva/jwt-go"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth"
|
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth"
|
||||||
@ -26,7 +28,7 @@ type TokenGenerator interface {
|
|||||||
// Authenticate finds a user by their email and verifies their password. On success
|
// Authenticate finds a user by their email and verifies their password. On success
|
||||||
// it returns a Token that can be used to authenticate access to the application in
|
// it returns a Token that can be used to authenticate access to the application in
|
||||||
// the future.
|
// the future.
|
||||||
func Authenticate(ctx context.Context, dbConn *sqlx.DB, tknGen TokenGenerator, email, password string, expires time.Duration, now time.Time) (Token, error) {
|
func Authenticate(ctx context.Context, dbConn *sqlx.DB, tknGen TokenGenerator, email, password string, expires time.Duration, now time.Time, scopes ...string) (Token, error) {
|
||||||
span, ctx := tracer.StartSpanFromContext(ctx, "internal.user.Authenticate")
|
span, ctx := tracer.StartSpanFromContext(ctx, "internal.user.Authenticate")
|
||||||
defer span.Finish()
|
defer span.Finish()
|
||||||
|
|
||||||
@ -58,13 +60,13 @@ func Authenticate(ctx context.Context, dbConn *sqlx.DB, tknGen TokenGenerator, e
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The user is successfully authenticated with the supplied email and password.
|
// The user is successfully authenticated with the supplied email and password.
|
||||||
return generateToken(ctx, dbConn, tknGen, auth.Claims{}, u.ID, "", expires, now)
|
return generateToken(ctx, dbConn, tknGen, auth.Claims{}, u.ID, "", expires, now, scopes...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Authenticate finds a user by their email and verifies their password. On success
|
// Authenticate finds a user by their email and verifies their password. On success
|
||||||
// it returns a Token that can be used to authenticate access to the application in
|
// it returns a Token that can be used to authenticate access to the application in
|
||||||
// the future.
|
// the future.
|
||||||
func SwitchAccount(ctx context.Context, dbConn *sqlx.DB, tknGen TokenGenerator, claims auth.Claims, accountID string, expires time.Duration, now time.Time) (Token, error) {
|
func SwitchAccount(ctx context.Context, dbConn *sqlx.DB, tknGen TokenGenerator, claims auth.Claims, accountID string, expires time.Duration, now time.Time, scopes ...string) (Token, error) {
|
||||||
span, ctx := tracer.StartSpanFromContext(ctx, "internal.user.SwitchAccount")
|
span, ctx := tracer.StartSpanFromContext(ctx, "internal.user.SwitchAccount")
|
||||||
defer span.Finish()
|
defer span.Finish()
|
||||||
|
|
||||||
@ -86,12 +88,12 @@ func SwitchAccount(ctx context.Context, dbConn *sqlx.DB, tknGen TokenGenerator,
|
|||||||
// Generate a token for the user ID in supplied in claims as the Subject. Pass
|
// Generate a token for the user ID in supplied in claims as the Subject. Pass
|
||||||
// in the supplied claims as well to enforce ACLs when finding the current
|
// in the supplied claims as well to enforce ACLs when finding the current
|
||||||
// list of accounts for the user.
|
// list of accounts for the user.
|
||||||
return generateToken(ctx, dbConn, tknGen, claims, req.UserID, req.AccountID, expires, now)
|
return generateToken(ctx, dbConn, tknGen, claims, req.UserID, req.AccountID, expires, now, scopes...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// generateToken generates claims for the supplied user ID and account ID and then
|
// generateToken generates claims for the supplied user ID and account ID and then
|
||||||
// returns the token for the generated claims used for authentication.
|
// returns the token for the generated claims used for authentication.
|
||||||
func generateToken(ctx context.Context, dbConn *sqlx.DB, tknGen TokenGenerator, claims auth.Claims, userID, accountID string, expires time.Duration, now time.Time) (Token, error) {
|
func generateToken(ctx context.Context, dbConn *sqlx.DB, tknGen TokenGenerator, claims auth.Claims, userID, accountID string, expires time.Duration, now time.Time, scopes ...string) (Token, error) {
|
||||||
|
|
||||||
type userAccount struct {
|
type userAccount struct {
|
||||||
AccountID string
|
AccountID string
|
||||||
@ -100,13 +102,16 @@ func generateToken(ctx context.Context, dbConn *sqlx.DB, tknGen TokenGenerator,
|
|||||||
UserArchived pq.NullTime
|
UserArchived pq.NullTime
|
||||||
AccountStatus string
|
AccountStatus string
|
||||||
AccountArchived pq.NullTime
|
AccountArchived pq.NullTime
|
||||||
|
AccountTimezone sql.NullString
|
||||||
|
UserTimezone sql.NullString
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build select statement for users_accounts table to find all the user accounts for the user
|
// Build select statement for users_accounts table to find all the user accounts for the user
|
||||||
f := func() ([]userAccount, error) {
|
f := func() ([]userAccount, error) {
|
||||||
query := sqlbuilder.NewSelectBuilder().Select("ua.account_id, ua.roles, ua.status as userStatus, ua.archived_at userArchived, a.status as accountStatus, a.archived_at as accountArchived").
|
query := sqlbuilder.NewSelectBuilder().Select("ua.account_id, ua.roles, ua.status as userStatus, ua.archived_at userArchived, a.status as accountStatus, a.archived_at, a.timezone, u.timezone as accountArchived").
|
||||||
From(userAccountTableName+" ua").
|
From(userAccountTableName+" ua").
|
||||||
Join(accountTableName+" a", "a.id = ua.account_id")
|
Join(accountTableName+" a", "a.id = ua.account_id").
|
||||||
|
Join(userTableName+" u", "u.id = ua.user_id")
|
||||||
query.Where(query.And(
|
query.Where(query.And(
|
||||||
query.Equal("ua.user_id", userID),
|
query.Equal("ua.user_id", userID),
|
||||||
))
|
))
|
||||||
@ -125,7 +130,7 @@ func generateToken(ctx context.Context, dbConn *sqlx.DB, tknGen TokenGenerator,
|
|||||||
var resp []userAccount
|
var resp []userAccount
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var ua userAccount
|
var ua userAccount
|
||||||
err = rows.Scan(&ua.AccountID, &ua.Roles, &ua.UserStatus, &ua.UserArchived, &ua.AccountStatus, &ua.AccountArchived)
|
err = rows.Scan(&ua.AccountID, &ua.Roles, &ua.UserStatus, &ua.UserArchived, &ua.AccountStatus, &ua.AccountArchived, &ua.AccountTimezone, &ua.UserTimezone)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, errors.WithStack(err)
|
||||||
}
|
}
|
||||||
@ -212,11 +217,64 @@ func generateToken(ctx context.Context, dbConn *sqlx.DB, tknGen TokenGenerator,
|
|||||||
accountIds = append(accountIds, a.AccountID)
|
accountIds = append(accountIds, a.AccountID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Allow the scope to be defined for the claims. This enables testing via the API when a user has the role of admin
|
||||||
|
// and would like to limit their role to user.
|
||||||
|
var roles []string
|
||||||
|
if len(scopes) > 0 && scopes[0] != "" {
|
||||||
|
// Parse scopes, handle when one value has a list of scopes
|
||||||
|
// separated by a space.
|
||||||
|
var scopeList []string
|
||||||
|
for _, vs := range scopes {
|
||||||
|
for _, v := range strings.Split(vs, " ") {
|
||||||
|
v = strings.TrimSpace(v)
|
||||||
|
if v == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
scopeList = append(scopeList, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range scopeList {
|
||||||
|
var scopeValid bool
|
||||||
|
for _, r := range account.Roles {
|
||||||
|
if r == s || (s == auth.RoleUser && r == auth.RoleAdmin) {
|
||||||
|
scopeValid = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if scopeValid {
|
||||||
|
roles = append(roles, s)
|
||||||
|
} else {
|
||||||
|
err := errors.Errorf("invalid scope '%s'", s)
|
||||||
|
return Token{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
roles = account.Roles
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(roles) == 0 {
|
||||||
|
err := errors.New("no roles defined for user")
|
||||||
|
return Token{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the timezone if one is specifically set on the user.
|
||||||
|
var tz *time.Location
|
||||||
|
if account.UserTimezone.Valid && account.UserTimezone.String != "" {
|
||||||
|
tz, _ = time.LoadLocation(account.UserTimezone.String)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If user timezone failed to parse or none is set, check the timezone set on the account.
|
||||||
|
if tz == nil && account.AccountTimezone.Valid && account.AccountTimezone.String != "" {
|
||||||
|
tz, _ = time.LoadLocation(account.AccountTimezone.String)
|
||||||
|
}
|
||||||
|
|
||||||
// JWT claims requires both an audience and a subject. For this application:
|
// JWT claims requires both an audience and a subject. For this application:
|
||||||
// Subject: The ID of the user authenticated.
|
// Subject: The ID of the user authenticated.
|
||||||
// Audience: The ID of the account the user is accessing. A list of account IDs
|
// Audience: The ID of the account the user is accessing. A list of account IDs
|
||||||
// will also be included to support the user switching between them.
|
// will also be included to support the user switching between them.
|
||||||
claims = auth.NewClaims(userID, accountID, accountIds, account.Roles, now, expires)
|
claims = auth.NewClaims(userID, accountID, accountIds, roles, tz, now, expires)
|
||||||
|
|
||||||
// Generate a token for the user with the defined claims.
|
// Generate a token for the user with the defined claims.
|
||||||
tknStr, err := tknGen.GenerateToken(claims)
|
tknStr, err := tknGen.GenerateToken(claims)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package user
|
package user
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -109,12 +110,13 @@ func TestAuthenticate(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Log("\t\tGot :", err)
|
t.Log("\t\tGot :", err)
|
||||||
t.Fatalf("\t%s\tParse claims from token failed.", tests.Failed)
|
t.Fatalf("\t%s\tParse claims from token failed.", tests.Failed)
|
||||||
} else if diff := cmp.Diff(claims1, tkn1.claims); diff != "" {
|
}
|
||||||
|
|
||||||
|
// Hack for Unhandled Exception in go-cmp@v0.3.0/cmp/options.go:229
|
||||||
|
resClaims, _ := json.Marshal(claims1)
|
||||||
|
expectClaims, _ := json.Marshal(tkn1.claims)
|
||||||
|
if diff := cmp.Diff(string(resClaims), string(expectClaims)); diff != "" {
|
||||||
t.Fatalf("\t%s\tExpected parsed claims to match from token. Diff:\n%s", tests.Failed, diff)
|
t.Fatalf("\t%s\tExpected parsed claims to match from token. Diff:\n%s", tests.Failed, diff)
|
||||||
} else if diff := cmp.Diff(claims1.Roles, []string{account1Role}); diff != "" {
|
|
||||||
t.Fatalf("\t%s\tExpected parsed claims roles to match user account. Diff:\n%s", tests.Failed, diff)
|
|
||||||
} else if diff := cmp.Diff(claims1.AccountIds, []string{account1Id, account2Id}); diff != "" {
|
|
||||||
t.Fatalf("\t%s\tExpected parsed claims account IDs to match the single user account. Diff:\n%s", tests.Failed, diff)
|
|
||||||
}
|
}
|
||||||
t.Logf("\t%s\tAuthenticate parse claims from token ok.", tests.Success)
|
t.Logf("\t%s\tAuthenticate parse claims from token ok.", tests.Success)
|
||||||
|
|
||||||
@ -131,12 +133,13 @@ func TestAuthenticate(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Log("\t\tGot :", err)
|
t.Log("\t\tGot :", err)
|
||||||
t.Fatalf("\t%s\tParse claims from token failed.", tests.Failed)
|
t.Fatalf("\t%s\tParse claims from token failed.", tests.Failed)
|
||||||
} else if diff := cmp.Diff(claims2, tkn2.claims); diff != "" {
|
}
|
||||||
|
|
||||||
|
// Hack for Unhandled Exception in go-cmp@v0.3.0/cmp/options.go:229
|
||||||
|
resClaims, _ = json.Marshal(claims2)
|
||||||
|
expectClaims, _ = json.Marshal(tkn2.claims)
|
||||||
|
if diff := cmp.Diff(string(resClaims), string(expectClaims)); diff != "" {
|
||||||
t.Fatalf("\t%s\tExpected parsed claims to match from token. Diff:\n%s", tests.Failed, diff)
|
t.Fatalf("\t%s\tExpected parsed claims to match from token. Diff:\n%s", tests.Failed, diff)
|
||||||
} else if diff := cmp.Diff(claims2.Roles, []string{account2Role}); diff != "" {
|
|
||||||
t.Fatalf("\t%s\tExpected parsed claims roles to match user account. Diff:\n%s", tests.Failed, diff)
|
|
||||||
} else if diff := cmp.Diff(claims2.AccountIds, []string{account1Id, account2Id}); diff != "" {
|
|
||||||
t.Fatalf("\t%s\tExpected parsed claims account IDs to match the single user account. Diff:\n%s", tests.Failed, diff)
|
|
||||||
}
|
}
|
||||||
t.Logf("\t%s\tSwitchAccount parse claims from token ok.", tests.Success)
|
t.Logf("\t%s\tSwitchAccount parse claims from token ok.", tests.Success)
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
package user
|
package user
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"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"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/lib/pq"
|
"github.com/lib/pq"
|
||||||
@ -10,21 +12,53 @@ import (
|
|||||||
|
|
||||||
// User represents someone with access to our system.
|
// User represents someone with access to our system.
|
||||||
type User struct {
|
type User struct {
|
||||||
ID string `json:"id" example:"d69bdef7-173f-4d29-b52c-3edc60baf6a2"`
|
ID string `json:"id" validate:"required,uuid" example:"d69bdef7-173f-4d29-b52c-3edc60baf6a2"`
|
||||||
Name string `json:"name" validate:"required" example:"Gabi May"`
|
Name string `json:"name" validate:"required" example:"Gabi May"`
|
||||||
Email string `json:"email" example:"gabi@geeksinthewoods.com"`
|
Email string `json:"email" validate:"required,email,unique" example:"gabi@geeksinthewoods.com"`
|
||||||
|
PasswordSalt string `json:"-" validate:"required"`
|
||||||
PasswordSalt string `json:"-"`
|
PasswordHash []byte `json:"-" validate:"required"`
|
||||||
PasswordHash []byte `json:"-"`
|
|
||||||
PasswordReset *sql.NullString `json:"-"`
|
PasswordReset *sql.NullString `json:"-"`
|
||||||
|
Timezone string `json:"timezone" validate:"omitempty" example:"America/Anchorage"`
|
||||||
Timezone string `json:"timezone" example:"America/Anchorage"`
|
|
||||||
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
ArchivedAt *pq.NullTime `json:"archived_at,omitempty"`
|
ArchivedAt *pq.NullTime `json:"archived_at,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UserResponse represents someone with access to our system that is returned for display.
|
||||||
|
type UserResponse struct {
|
||||||
|
ID string `json:"id" example:"d69bdef7-173f-4d29-b52c-3edc60baf6a2"`
|
||||||
|
Name string `json:"name" example:"Gabi May"`
|
||||||
|
Email string `json:"email" example:"gabi@geeksinthewoods.com"`
|
||||||
|
Timezone string `json:"timezone" example:"America/Anchorage"`
|
||||||
|
CreatedAt web.TimeResponse `json:"created_at"` // CreatedAt contains multiple format options for display.
|
||||||
|
UpdatedAt web.TimeResponse `json:"updated_at"` // UpdatedAt contains multiple format options for display.
|
||||||
|
ArchivedAt *web.TimeResponse `json:"archived_at,omitempty"` // ArchivedAt contains multiple format options for display.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response transforms User and UserResponse that is used for display.
|
||||||
|
// Additional filtering by context values or translations could be applied.
|
||||||
|
func (m *User) Response(ctx context.Context) *UserResponse {
|
||||||
|
if m == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
r := &UserResponse{
|
||||||
|
ID: m.ID,
|
||||||
|
Name: m.Name,
|
||||||
|
Email: m.Email,
|
||||||
|
Timezone: m.Timezone,
|
||||||
|
CreatedAt: web.NewTimeResponse(ctx, m.CreatedAt),
|
||||||
|
UpdatedAt: web.NewTimeResponse(ctx, m.UpdatedAt),
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.ArchivedAt != nil && !m.ArchivedAt.Time.IsZero() {
|
||||||
|
at := web.NewTimeResponse(ctx, m.ArchivedAt.Time)
|
||||||
|
r.ArchivedAt = &at
|
||||||
|
}
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
// UserCreateRequest contains information needed to create a new User.
|
// UserCreateRequest contains information needed to create a new User.
|
||||||
type UserCreateRequest struct {
|
type UserCreateRequest struct {
|
||||||
Name string `json:"name" validate:"required" example:"Gabi May"`
|
Name string `json:"name" validate:"required" example:"Gabi May"`
|
||||||
@ -41,28 +75,34 @@ type UserCreateRequest struct {
|
|||||||
// we do not want to use pointers to basic types but we make exceptions around
|
// we do not want to use pointers to basic types but we make exceptions around
|
||||||
// marshalling/unmarshalling.
|
// marshalling/unmarshalling.
|
||||||
type UserUpdateRequest struct {
|
type UserUpdateRequest struct {
|
||||||
ID string `json:"id" validate:"required,uuid"`
|
ID string `json:"id" validate:"required,uuid" example:"d69bdef7-173f-4d29-b52c-3edc60baf6a2"`
|
||||||
Name *string `json:"name,omitempty" validate:"omitempty"`
|
Name *string `json:"name,omitempty" validate:"omitempty" example:"Gabi May Not"`
|
||||||
Email *string `json:"email,omitempty" validate:"omitempty,email,unique"`
|
Email *string `json:"email,omitempty" validate:"omitempty,email,unique" example:"gabi.may@geeksinthewoods.com"`
|
||||||
Timezone *string `json:"timezone,omitempty" validate:"omitempty"`
|
Timezone *string `json:"timezone,omitempty" validate:"omitempty" example:"America/Anchorage"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserUpdatePasswordRequest defines what information is required to update a user password.
|
// UserUpdatePasswordRequest defines what information is required to update a user password.
|
||||||
type UserUpdatePasswordRequest struct {
|
type UserUpdatePasswordRequest struct {
|
||||||
ID string `json:"id" validate:"required,uuid"`
|
ID string `json:"id" validate:"required,uuid" example:"d69bdef7-173f-4d29-b52c-3edc60baf6a2"`
|
||||||
Password string `json:"password" validate:"required"`
|
Password string `json:"password" validate:"required" example:"NeverTellSecret"`
|
||||||
PasswordConfirm string `json:"password_confirm" validate:"omitempty,eqfield=Password"`
|
PasswordConfirm string `json:"password_confirm" validate:"omitempty,eqfield=Password" example:"NeverTellSecret"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserArchiveRequest defines the information needed to archive an user. This will archive (soft-delete) the
|
||||||
|
// existing database entry.
|
||||||
|
type UserArchiveRequest struct {
|
||||||
|
ID string `json:"id" validate:"required,uuid" example:"d69bdef7-173f-4d29-b52c-3edc60baf6a2"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserFindRequest defines the possible options to search for users. By default
|
// UserFindRequest defines the possible options to search for users. By default
|
||||||
// archived users will be excluded from response.
|
// archived users will be excluded from response.
|
||||||
type UserFindRequest struct {
|
type UserFindRequest struct {
|
||||||
Where *string `json:"where"`
|
Where *string `json:"where" example:"name = ? and email = ?"`
|
||||||
Args []interface{} `json:"args" swaggertype:"array,string"`
|
Args []interface{} `json:"args" swaggertype:"array,string" example:"Company Name,gabi.may@geeksinthewoods.com"`
|
||||||
Order []string `json:"order"`
|
Order []string `json:"order" example:"created_at desc"`
|
||||||
Limit *uint `json:"limit"`
|
Limit *uint `json:"limit" example:"10"`
|
||||||
Offset *uint `json:"offset"`
|
Offset *uint `json:"offset" example:"20"`
|
||||||
IncludedArchived bool `json:"included-archived"`
|
IncludedArchived bool `json:"included-archived" example:"false"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Token is the payload we deliver to users when they authenticate.
|
// Token is the payload we deliver to users when they authenticate.
|
||||||
|
@ -512,18 +512,19 @@ func UpdatePassword(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, re
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Archive soft deleted the user by ID from the database.
|
||||||
|
func ArchiveById(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, id string, now time.Time) error {
|
||||||
|
req := UserArchiveRequest{
|
||||||
|
ID: id,
|
||||||
|
}
|
||||||
|
return Archive(ctx, claims, dbConn, req, now)
|
||||||
|
}
|
||||||
|
|
||||||
// Archive soft deleted the user from the database.
|
// Archive soft deleted the user from the database.
|
||||||
func Archive(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, userID string, now time.Time) error {
|
func Archive(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req UserArchiveRequest, now time.Time) error {
|
||||||
span, ctx := tracer.StartSpanFromContext(ctx, "internal.user.Archive")
|
span, ctx := tracer.StartSpanFromContext(ctx, "internal.user.Archive")
|
||||||
defer span.Finish()
|
defer span.Finish()
|
||||||
|
|
||||||
// Defines the struct to apply validation
|
|
||||||
req := struct {
|
|
||||||
ID string `validate:"required,uuid"`
|
|
||||||
}{
|
|
||||||
ID: userID,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate the request.
|
// Validate the request.
|
||||||
err := validator.New().Struct(req)
|
err := validator.New().Struct(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -827,7 +827,7 @@ func TestCrud(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Archive (soft-delete) the user.
|
// Archive (soft-delete) the user.
|
||||||
err = Archive(ctx, tt.claims(user, accountId), test.MasterDB, user.ID, now)
|
err = ArchiveById(ctx, tt.claims(user, accountId), test.MasterDB, user.ID, now)
|
||||||
if err != nil && errors.Cause(err) != tt.updateErr {
|
if err != nil && errors.Cause(err) != tt.updateErr {
|
||||||
t.Logf("\t\tGot : %+v", err)
|
t.Logf("\t\tGot : %+v", err)
|
||||||
t.Logf("\t\tWant: %+v", tt.updateErr)
|
t.Logf("\t\tWant: %+v", tt.updateErr)
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
package user_account
|
package user_account
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"database/sql/driver"
|
"database/sql/driver"
|
||||||
|
"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"
|
||||||
@ -17,16 +19,53 @@ import (
|
|||||||
// application. The status will allow users to be managed on by account with users
|
// application. The status will allow users to be managed on by account with users
|
||||||
// being global to the application.
|
// being global to the application.
|
||||||
type UserAccount struct {
|
type UserAccount struct {
|
||||||
ID string `json:"id" example:"72938896-a998-4258-a17b-6418dcdb80e3"`
|
ID string `json:"id" validate:"required,uuid" example:"72938896-a998-4258-a17b-6418dcdb80e3"`
|
||||||
UserID string `json:"user_id" example:"d69bdef7-173f-4d29-b52c-3edc60baf6a2"`
|
UserID string `json:"user_id" validate:"required,uuid" example:"d69bdef7-173f-4d29-b52c-3edc60baf6a2"`
|
||||||
AccountID string `json:"account_id" example:"c4653bf9-5978-48b7-89c5-95704aebb7e2"`
|
AccountID string `json:"account_id" validate:"required,uuid" example:"c4653bf9-5978-48b7-89c5-95704aebb7e2"`
|
||||||
Roles UserAccountRoles `json:"roles" swaggertype:"array,string" enums:"admin,user" example:"admin"`
|
Roles UserAccountRoles `json:"roles" validate:"required,dive,oneof=admin user" enums:"admin,user" swaggertype:"array,string" example:"admin"`
|
||||||
Status UserAccountStatus `json:"status" swaggertype:"string" enums:"active,invited,disabled" example:"active"`
|
Status UserAccountStatus `json:"status" validate:"omitempty,oneof=active invited disabled" enums:"active,invited,disabled" swaggertype:"string" example:"active"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
ArchivedAt *pq.NullTime `json:"archived_at,omitempty"`
|
ArchivedAt *pq.NullTime `json:"archived_at,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UserAccountResponse defines the one to many relationship of an user to an account that is returned for display.
|
||||||
|
type UserAccountResponse struct {
|
||||||
|
ID string `json:"id" example:"d69bdef7-173f-4d29-b52c-3edc60baf6a2"`
|
||||||
|
UserID string `json:"user_id" example:"d69bdef7-173f-4d29-b52c-3edc60baf6a2"`
|
||||||
|
AccountID string `json:"account_id" example:"c4653bf9-5978-48b7-89c5-95704aebb7e2"`
|
||||||
|
Roles UserAccountRoles `json:"roles" validate:"required,dive,oneof=admin user" enums:"admin,user" swaggertype:"array,string" example:"admin"`
|
||||||
|
Status web.EnumResponse `json:"status"` // Status is enum with values [active, invited, disabled].
|
||||||
|
CreatedAt web.TimeResponse `json:"created_at"` // CreatedAt contains multiple format options for display.
|
||||||
|
UpdatedAt web.TimeResponse `json:"updated_at"` // UpdatedAt contains multiple format options for display.
|
||||||
|
ArchivedAt *web.TimeResponse `json:"archived_at,omitempty"` // ArchivedAt contains multiple format options for display.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response transforms UserAccount and UserAccountResponse that is used for display.
|
||||||
|
// Additional filtering by context values or translations could be applied.
|
||||||
|
func (m *UserAccount) Response(ctx context.Context) *UserAccountResponse {
|
||||||
|
if m == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
r := &UserAccountResponse{
|
||||||
|
ID: m.ID,
|
||||||
|
UserID: m.UserID,
|
||||||
|
AccountID: m.AccountID,
|
||||||
|
Roles: m.Roles,
|
||||||
|
Status: web.NewEnumResponse(ctx, m.Status, UserAccountRole_Values),
|
||||||
|
CreatedAt: web.NewTimeResponse(ctx, m.CreatedAt),
|
||||||
|
UpdatedAt: web.NewTimeResponse(ctx, m.UpdatedAt),
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.ArchivedAt != nil && !m.ArchivedAt.Time.IsZero() {
|
||||||
|
at := web.NewTimeResponse(ctx, m.ArchivedAt.Time)
|
||||||
|
r.ArchivedAt = &at
|
||||||
|
}
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
// CreateUserAccountRequest defines the information is needed to associate a user to an
|
// CreateUserAccountRequest defines the information is needed to associate a user to an
|
||||||
// account. Users are global to the application and each users access can be managed
|
// account. Users are global to the application and each users access can be managed
|
||||||
// on an account level. If a current entry exists in the database but is archived,
|
// on an account level. If a current entry exists in the database but is archived,
|
||||||
@ -41,8 +80,8 @@ type CreateUserAccountRequest struct {
|
|||||||
// UpdateUserAccountRequest defines the information needed to update the roles or the
|
// UpdateUserAccountRequest defines the information needed to update the roles or the
|
||||||
// status for an existing user account.
|
// status for an existing user account.
|
||||||
type UpdateUserAccountRequest struct {
|
type UpdateUserAccountRequest struct {
|
||||||
UserID string `json:"user_id" validate:"required,uuid"`
|
UserID string `json:"user_id" validate:"required,uuid" example:"d69bdef7-173f-4d29-b52c-3edc60baf6a2"`
|
||||||
AccountID string `json:"account_id" validate:"required,uuid"`
|
AccountID string `json:"account_id" validate:"required,uuid" example:"c4653bf9-5978-48b7-89c5-95704aebb7e2"`
|
||||||
Roles *UserAccountRoles `json:"roles,omitempty" validate:"required,dive,oneof=admin user" enums:"admin,user" swaggertype:"array,string" example:"user"`
|
Roles *UserAccountRoles `json:"roles,omitempty" validate:"required,dive,oneof=admin user" enums:"admin,user" swaggertype:"array,string" example:"user"`
|
||||||
Status *UserAccountStatus `json:"status,omitempty" validate:"omitempty,oneof=active invited disabled" enums:"active,invited,disabled" swaggertype:"string" example:"disabled"`
|
Status *UserAccountStatus `json:"status,omitempty" validate:"omitempty,oneof=active invited disabled" enums:"active,invited,disabled" swaggertype:"string" example:"disabled"`
|
||||||
unArchive bool `json:"-"` // Internal use only.
|
unArchive bool `json:"-"` // Internal use only.
|
||||||
@ -51,26 +90,26 @@ type UpdateUserAccountRequest struct {
|
|||||||
// ArchiveUserAccountRequest defines the information needed to remove an existing account
|
// ArchiveUserAccountRequest defines the information needed to remove an existing account
|
||||||
// for a user. This will archive (soft-delete) the existing database entry.
|
// for a user. This will archive (soft-delete) the existing database entry.
|
||||||
type ArchiveUserAccountRequest struct {
|
type ArchiveUserAccountRequest struct {
|
||||||
UserID string `json:"user_id" validate:"required,uuid"`
|
UserID string `json:"user_id" validate:"required,uuid" example:"d69bdef7-173f-4d29-b52c-3edc60baf6a2"`
|
||||||
AccountID string `json:"account_id" validate:"required,uuid"`
|
AccountID string `json:"account_id" validate:"required,uuid" example:"c4653bf9-5978-48b7-89c5-95704aebb7e2"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteUserAccountRequest defines the information needed to delete an existing account
|
// DeleteUserAccountRequest defines the information needed to delete an existing account
|
||||||
// for a user. This will hard delete the existing database entry.
|
// for a user. This will hard delete the existing database entry.
|
||||||
type DeleteUserAccountRequest struct {
|
type DeleteUserAccountRequest struct {
|
||||||
UserID string `json:"user_id" validate:"required,uuid"`
|
UserID string `json:"user_id" validate:"required,uuid" example:"d69bdef7-173f-4d29-b52c-3edc60baf6a2"`
|
||||||
AccountID string `json:"account_id" validate:"required,uuid"`
|
AccountID string `json:"account_id" validate:"required,uuid" example:"c4653bf9-5978-48b7-89c5-95704aebb7e2"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserAccountFindRequest defines the possible options to search for users accounts.
|
// UserAccountFindRequest defines the possible options to search for users accounts.
|
||||||
// By default archived user accounts will be excluded from response.
|
// By default archived user accounts will be excluded from response.
|
||||||
type UserAccountFindRequest struct {
|
type UserAccountFindRequest struct {
|
||||||
Where *string `json:"where"`
|
Where *string `json:"where" example:"user_id = ? and account_id = ?"`
|
||||||
Args []interface{} `json:"args" swaggertype:"array,string"`
|
Args []interface{} `json:"args" swaggertype:"array,string" example:"d69bdef7-173f-4d29-b52c-3edc60baf6a2,c4653bf9-5978-48b7-89c5-95704aebb7e2"`
|
||||||
Order []string `json:"order"`
|
Order []string `json:"order" example:"created_at desc"`
|
||||||
Limit *uint `json:"limit"`
|
Limit *uint `json:"limit" example:"10"`
|
||||||
Offset *uint `json:"offset"`
|
Offset *uint `json:"offset" example:"20"`
|
||||||
IncludedArchived bool `json:"included-archived"`
|
IncludedArchived bool `json:"included-archived" example:"false"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserAccountStatus represents the status of a user for an account.
|
// UserAccountStatus represents the status of a user for an account.
|
||||||
|
Reference in New Issue
Block a user