1
0
mirror of https://github.com/raseels-repos/golang-saas-starter-kit.git synced 2025-06-17 00:17:59 +02:00

Completed API documentation for swagger

This commit is contained in:
Lee Brown
2019-06-25 22:31:54 -08:00
parent 8328cf525b
commit d6b6b605a4
28 changed files with 4763 additions and 491 deletions

View File

@ -6,7 +6,11 @@ accelerator@geeksinthewoods.com.com
## 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

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

View File

@ -10,6 +10,7 @@ import (
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web"
"github.com/jmoiron/sqlx"
"github.com/pkg/errors"
"gopkg.in/go-playground/validator.v9"
)
// Account represents the Account API method handler set.
@ -19,39 +20,19 @@ type Account struct {
// 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
// @Summary Read returns the specified account from the system.
// @Description get string by ID
// @Summary Get account by ID
// @Description Read returns the specified account from the system.
// @Tags account
// @Accept json
// @Produce json
// @Security OAuth2Password
// @Param id path string true "Account ID"
// @Success 200 {object} account.Account
// @Header 200 {string} Token "qwerty"
// @Failure 400 {object} web.Error
// @Failure 403 {object} web.Error
// @Failure 404 {object} web.Error
// @Success 200 {object} account.AccountResponse
// @Failure 400 {object} web.ErrorResponse
// @Failure 403 {object} web.ErrorResponse
// @Failure 404 {object} web.ErrorResponse
// @Failure 500 {object} web.ErrorResponse
// @Router /accounts/{id} [get]
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)
@ -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.
func (a *Account) Create(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
v, ok := ctx.Value(web.KeyValues).(*web.Values)
if !ok {
return web.NewShutdownError("web value missing from context")
}
claims, ok := ctx.Value(auth.Key).(auth.Claims)
if !ok {
return errors.New("claims missing from context")
}
var req account.AccountCreateRequest
if err := web.Decode(r, &req); err != nil {
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.
// Read godoc
// @Summary Update account by ID
// @Description Update updates the specified account in the system.
// @Tags account
// @Accept json
// @Produce json
// @Security OAuth2Password
// @Param data body account.AccountUpdateRequest 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 /accounts [patch]
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)
if !ok {
@ -129,9 +93,14 @@ func (a *Account) Update(ctx context.Context, w http.ResponseWriter, r *http.Req
var req account.AccountUpdateRequest
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)
if err != nil {
@ -143,62 +112,14 @@ func (a *Account) Update(ctx context.Context, w http.ResponseWriter, r *http.Req
case account.ErrForbidden:
return web.NewRequestError(err, http.StatusForbidden)
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 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)
}

View File

@ -4,12 +4,14 @@ import (
"context"
"net/http"
"strconv"
"strings"
"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/project"
"github.com/jmoiron/sqlx"
"github.com/pkg/errors"
"gopkg.in/go-playground/validator.v9"
)
// Project represents the Project API method handler set.
@ -19,7 +21,23 @@ type Project struct {
// 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 {
claims, ok := ctx.Value(auth.Key).(auth.Claims)
if !ok {
@ -27,8 +45,57 @@ func (p *Project) Find(ctx context.Context, w http.ResponseWriter, r *http.Reque
}
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)
@ -36,10 +103,28 @@ func (p *Project) Find(ctx context.Context, w http.ResponseWriter, r *http.Reque
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 {
claims, ok := ctx.Value(auth.Key).(auth.Claims)
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 {
v, ok := ctx.Value(web.KeyValues).(*web.Values)
if !ok {
@ -86,7 +184,13 @@ func (p *Project) Create(ctx context.Context, w http.ResponseWriter, r *http.Req
var req project.ProjectCreateRequest
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)
@ -95,14 +199,31 @@ func (p *Project) Create(ctx context.Context, w http.ResponseWriter, r *http.Req
case project.ErrForbidden:
return web.NewRequestError(err, http.StatusForbidden)
default:
_, ok := err.(validator.ValidationErrors)
if ok {
return web.NewRequestError(err, http.StatusBadRequest)
}
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 {
v, ok := ctx.Value(web.KeyValues).(*web.Values)
if !ok {
@ -116,9 +237,14 @@ func (p *Project) Update(ctx context.Context, w http.ResponseWriter, r *http.Req
var req project.ProjectUpdateRequest
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)
if err != nil {
@ -130,14 +256,31 @@ func (p *Project) Update(ctx context.Context, w http.ResponseWriter, r *http.Req
case project.ErrForbidden:
return web.NewRequestError(err, http.StatusForbidden)
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)
}
// 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 {
v, ok := ctx.Value(web.KeyValues).(*web.Values)
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")
}
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 {
switch err {
case project.ErrInvalidID:
@ -159,14 +313,32 @@ func (p *Project) Archive(ctx context.Context, w http.ResponseWriter, r *http.Re
case project.ErrForbidden:
return web.NewRequestError(err, http.StatusForbidden)
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)
}
// 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 {
claims, ok := ctx.Value(auth.Key).(auth.Claims)
if !ok {

View File

@ -30,14 +30,15 @@ func API(shutdown chan os.Signal, log *log.Logger, masterDB *sqlx.DB, redis *red
MasterDB: masterDB,
TokenGenerator: 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("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/:id/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", u.Update, mid.Authenticate(authenticator))
app.Handle("PATCH", "/v1/users/password", u.UpdatePassword, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
app.Handle("PATCH", "/v1/users/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("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
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{
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("PATCH", "/v1/accounts/:id", 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))
app.Handle("PATCH", "/v1/accounts", a.Update, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
// Register signup endpoints.
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("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("PATCH", "/v1/projects/:id", 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", p.Update, 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))
// Register swagger documentation.
@ -78,3 +75,12 @@ func API(shutdown chan os.Signal, log *log.Logger, masterDB *sqlx.DB, redis *red
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() {}

View File

@ -2,13 +2,15 @@ package handlers
import (
"context"
"net/http"
"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/web"
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/signup"
"github.com/jmoiron/sqlx"
"github.com/pkg/errors"
"net/http"
"gopkg.in/go-playground/validator.v9"
)
// Signup represents the Signup API method handler set.
@ -26,9 +28,9 @@ type Signup struct {
// @Produce json
// @Param data body signup.SignupRequest true "Signup details"
// @Success 200 {object} signup.SignupResponse
// @Header 200 {string} Token "qwerty"
// @Failure 400 {object} web.Error
// @Failure 403 {object} web.Error
// @Failure 400 {object} web.ErrorResponse
// @Failure 403 {object} web.ErrorResponse
// @Failure 500 {object} web.ErrorResponse
// @Router /signup [post]
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)
@ -41,7 +43,13 @@ func (c *Signup) Signup(ctx context.Context, w http.ResponseWriter, r *http.Requ
var req signup.SignupRequest
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)
@ -50,7 +58,12 @@ func (c *Signup) Signup(ctx context.Context, w http.ResponseWriter, r *http.Requ
case account.ErrForbidden:
return web.NewRequestError(err, http.StatusForbidden)
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)
}
}

View File

@ -4,6 +4,7 @@ import (
"context"
"net/http"
"strconv"
"strings"
"time"
"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"
"github.com/jmoiron/sqlx"
"github.com/pkg/errors"
"gopkg.in/go-playground/validator.v9"
)
// sessionTtl defines the auth token expiration.
@ -24,7 +26,23 @@ type User struct {
// 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 {
claims, ok := ctx.Value(auth.Key).(auth.Claims)
if !ok {
@ -32,8 +50,67 @@ func (u *User) Find(ctx context.Context, w http.ResponseWriter, r *http.Request,
}
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 {
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)
@ -41,22 +118,27 @@ func (u *User) Find(ctx context.Context, w http.ResponseWriter, r *http.Request,
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
// @Summary Read returns the specified user from the system.
// @Description get string by ID
// @Summary Get user by ID
// @Description Read returns the specified user from the system.
// @Tags user
// @Accept json
// @Produce json
// @Security OAuth2Password
// @Param id path string true "User ID"
// @Success 200 {object} user.User
// @Header 200 {string} Token "qwerty"
// @Failure 400 {object} web.Error
// @Failure 403 {object} web.Error
// @Failure 404 {object} web.Error
// @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/{id} [get]
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)
@ -83,14 +165,32 @@ func (u *User) Read(ctx context.Context, w http.ResponseWriter, r *http.Request,
case user.ErrForbidden:
return web.NewRequestError(err, http.StatusForbidden)
default:
_, ok := err.(validator.ValidationErrors)
if ok {
return web.NewRequestError(err, http.StatusBadRequest)
}
return errors.Wrapf(err, "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 {
v, ok := ctx.Value(web.KeyValues).(*web.Values)
if !ok {
@ -104,7 +204,13 @@ func (u *User) Create(ctx context.Context, w http.ResponseWriter, r *http.Reques
var req user.UserCreateRequest
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)
@ -113,14 +219,32 @@ func (u *User) Create(ctx context.Context, w http.ResponseWriter, r *http.Reques
case user.ErrForbidden:
return web.NewRequestError(err, http.StatusForbidden)
default:
_, ok := err.(validator.ValidationErrors)
if ok {
return web.NewRequestError(err, http.StatusBadRequest)
}
return errors.Wrapf(err, "User: %+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 {
v, ok := ctx.Value(web.KeyValues).(*web.Values)
if !ok {
@ -134,9 +258,14 @@ func (u *User) Update(ctx context.Context, w http.ResponseWriter, r *http.Reques
var req user.UserUpdateRequest
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)
if err != nil {
@ -148,14 +277,32 @@ func (u *User) Update(ctx context.Context, w http.ResponseWriter, r *http.Reques
case user.ErrForbidden:
return web.NewRequestError(err, http.StatusForbidden)
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)
}
// 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 {
v, ok := ctx.Value(web.KeyValues).(*web.Values)
if !ok {
@ -169,9 +316,14 @@ func (u *User) UpdatePassword(ctx context.Context, w http.ResponseWriter, r *htt
var req user.UserUpdatePasswordRequest
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)
if err != nil {
@ -183,14 +335,32 @@ func (u *User) UpdatePassword(ctx context.Context, w http.ResponseWriter, r *htt
case user.ErrForbidden:
return web.NewRequestError(err, http.StatusForbidden)
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)
}
// 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 {
v, ok := ctx.Value(web.KeyValues).(*web.Values)
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")
}
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 {
switch err {
case user.ErrInvalidID:
@ -212,14 +393,32 @@ func (u *User) Archive(ctx context.Context, w http.ResponseWriter, r *http.Reque
case user.ErrForbidden:
return web.NewRequestError(err, http.StatusForbidden)
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)
}
// 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 {
claims, ok := ctx.Value(auth.Key).(auth.Claims)
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)
}
// 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 {
v, ok := ctx.Value(web.KeyValues).(*web.Values)
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")
}
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 {
switch err {
case user.ErrAuthenticationFailure:
return web.NewRequestError(err, http.StatusUnauthorized)
default:
_, ok := err.(validator.ValidationErrors)
if ok {
return web.NewRequestError(err, http.StatusBadRequest)
}
return errors.Wrap(err, "switch account")
}
}
@ -275,6 +492,7 @@ func (u *User) SwitchAccount(ctx context.Context, w http.ResponseWriter, r *http
// @Accept json
// @Produce json
// @Security BasicAuth
// @Param scope query string false "Scope" Enums(user, admin)
// @Success 200 {object} user.Token
// @Header 200 {string} Token "qwerty"
// @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)
}
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 {
switch err {
case user.ErrAuthenticationFailure:

View File

@ -55,9 +55,8 @@ var service = "WEB_API"
// @securitydefinitions.oauth2.password OAuth2Password
// @tokenUrl /v1/oauth/token
// @scope.read Grants read access
// @scope.write Grants write access
// @scope.admin Grants read and write access to administrative information
// @scope.user Grants basic privileges with role of user.
// @scope.admin Grants administrative privileges with role of admin.
func main() {