1
0
mirror of https://github.com/raseels-repos/golang-saas-starter-kit.git synced 2025-08-08 22:36:41 +02:00

Merge branch 'master' into issue8/datadog-lambda-func

This commit is contained in:
Lee Brown
2019-08-21 16:31:11 -08:00
69 changed files with 1995 additions and 1147 deletions

View File

@ -1,21 +1,25 @@
# SaaS Web API
Copyright 2019, Geeks Accelerator
accelerator@geeksinthewoods.com.com
twins@geeksaccelerator.com
## Description
Web API is a client facing API. Standard response format is JSON.
Web API is a client facing API. Standard response format is JSON. The example web-api service includes API documentation.
While the web app is meant for humans to experience and requires
a friendly UI, the web API is meant for customers or third-party partners of your SaaS to programmatically integrate. To
help show the similarities and differences between the pages in the web app and similar endpoints in the web API, we
have created this diagram below. Since it is very detailed, you can click on the image to see the larger version.
While the web app is meant for humans to experience and requires a friendly UI, the web API is meant for customers or
third-party partners of your SaaS to programmatically integrate. To help show the similarities and differences between
the pages in the web app and similar endpoints in the web API, we have created this diagram below. Since it is
very detailed, you can click on the image to see the larger version.
[![Diagram of pages in web app and endpoints in web API](resources/images/saas-starter-kit-pages-and-endpoints-800x600.png)](https://gitlab.com/geeks-accelerator/oss/saas-starter-kit/tree/master/resources/images/saas-starter-kit-pages-and-endpoints-800x600.png)
This web-api service is not directly used by the web-app service to prevent locking the functionally required for
internally development of the web-app service to the same functionality exposed to clients via this web-api service.
This separate web-api service can be exposed to clients and be maintained in a more rigid/structured process to manage
client expectations.
**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 it. This
@ -36,11 +40,17 @@ initial admin user must first be created. The initial admin user can easily be c
## API Documentation
Documentation for this API service is automatically generated using [swag](https://github.com/geeks-accelerator/swag). Once this
web-api service is running, it can be accessed at /docs
Documentation for this API service is automatically generated using [swag](https://github.com/geeks-accelerator/swag).
The Swag Go project also provides a web UI to allow you and your customers of your SaaS to explore your API - its exposed
business logic - as well as easily try our that exposed functionality.
Once this web-api service is running, the Swagger API documentation for the service can be accessed at /docs:
http://127.0.0.1:3001/docs/
You can refer to the example of the API documentation that we have deployed on production for you here:
https://api.example.saasstartupkit.com/docs/
[![Example Golang web app deployed](https://dzuyel7n94hma.cloudfront.net/saasstartupkit/assets/images/responsive/img/saas-startup-example-golang-project-web-api-documentation-swagger-ui.png/9334f34bf028e0656f73aeb9d931e726/saas-startup-example-golang-project-web-api-documentation-swagger-ui-320w-480w-800w.png)](https://api.example.saasstartupkit.com/docs/)
@ -295,8 +305,4 @@ Ensure the `pkg` directory used for go module cache has the correct permissions.
```bash
sudo chown -R $(whoami):staff ${HOME}/go/pkg
sudo chmod -R 755 ${HOME}/go/pkg
```
```

View File

@ -34,12 +34,13 @@
{"name": "ECS_SERVICE", "value": "{ECS_SERVICE}"},
{"name": "WEB_API_HTTP_HOST", "value": "{HTTP_HOST}"},
{"name": "WEB_API_HTTPS_HOST", "value": "{HTTPS_HOST}"},
{"name": "WEB_API_SERVICE_PROJECT", "value": "{APP_PROJECT}"},
{"name": "WEB_API_SERVICE_SERVICE_NAME", "value": "{SERVICE}"},
{"name": "WEB_API_SERVICE_BASE_URL", "value": "{APP_BASE_URL}"},
{"name": "WEB_API_SERVICE_HOST_NAMES", "value": "{HOST_NAMES}"},
{"name": "WEB_API_SERVICE_ENABLE_HTTPS", "value": "{HTTPS_ENABLED}"},
{"name": "WEB_API_SERVICE_EMAIL_SENDER", "value": "{EMAIL_SENDER}"},
{"name": "WEB_API_SERVICE_WEB_APP_BASE_URL", "value": "{WEB_APP_BASE_URL}"},
{"name": "WEB_API_PROJECT_PROJECT_NAME", "value": "{APP_PROJECT}"},
{"name": "WEB_API_PROJECT_EMAIL_SENDER", "value": "{EMAIL_SENDER}"},
{"name": "WEB_API_PROJECT_WEB_APP_BASE_URL", "value": "{WEB_APP_BASE_URL}"},
{"name": "WEB_API_REDIS_HOST", "value": "{CACHE_HOST}"},
{"name": "WEB_API_DB_HOST", "value": "{DB_HOST}"},
{"name": "WEB_API_DB_USER", "value": "{DB_USER}"},

View File

@ -4,24 +4,45 @@ import (
"context"
"net/http"
"strconv"
"time"
"geeks-accelerator/oss/saas-starter-kit/internal/account"
accountref "geeks-accelerator/oss/saas-starter-kit/internal/account/account_preference"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/auth"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/weberror"
"github.com/jmoiron/sqlx"
"github.com/pkg/errors"
"gopkg.in/go-playground/validator.v9"
)
// Account represents the Account API method handler set.
type Account struct {
MasterDB *sqlx.DB
type Accounts struct {
Repository AccountRepository
// ADD OTHER STATE LIKE THE LOGGER AND CONFIG HERE.
}
type AccountRepository interface {
//CanReadAccount(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, accountID string) error
Find(ctx context.Context, claims auth.Claims, req account.AccountFindRequest) (account.Accounts, error)
Create(ctx context.Context, claims auth.Claims, req account.AccountCreateRequest, now time.Time) (*account.Account, error)
ReadByID(ctx context.Context, claims auth.Claims, id string) (*account.Account, error)
Read(ctx context.Context, claims auth.Claims, req account.AccountReadRequest) (*account.Account, error)
Update(ctx context.Context, claims auth.Claims, req account.AccountUpdateRequest, now time.Time) error
Archive(ctx context.Context, claims auth.Claims, req account.AccountArchiveRequest, now time.Time) error
Delete(ctx context.Context, claims auth.Claims, req account.AccountDeleteRequest) error
}
type AccountPrefRepository interface {
Find(ctx context.Context, claims auth.Claims, req accountref.AccountPreferenceFindRequest) ([]*accountref.AccountPreference, error)
FindByAccountID(ctx context.Context, claims auth.Claims, req accountref.AccountPreferenceFindByAccountIDRequest) ([]*accountref.AccountPreference, error)
Read(ctx context.Context, claims auth.Claims, req accountref.AccountPreferenceReadRequest) (*accountref.AccountPreference, error)
Set(ctx context.Context, claims auth.Claims, req accountref.AccountPreferenceSetRequest, now time.Time) error
Archive(ctx context.Context, claims auth.Claims, req accountref.AccountPreferenceArchiveRequest, now time.Time) error
Delete(ctx context.Context, claims auth.Claims, req accountref.AccountPreferenceDeleteRequest) error
}
// Read godoc
// @Summary Get account by ID
// @Description Read returns the specified account from the system.
@ -35,7 +56,7 @@ type Account struct {
// @Failure 404 {object} weberror.ErrorResponse
// @Failure 500 {object} weberror.ErrorResponse
// @Router /accounts/{id} [get]
func (a *Account) Read(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
func (h *Accounts) Read(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
claims, ok := ctx.Value(auth.Key).(auth.Claims)
if !ok {
return errors.New("claims missing from context")
@ -52,7 +73,7 @@ func (a *Account) Read(ctx context.Context, w http.ResponseWriter, r *http.Reque
includeArchived = b
}
res, err := account.Read(ctx, claims, a.MasterDB, account.AccountReadRequest{
res, err := h.Repository.Read(ctx, claims, account.AccountReadRequest{
ID: params["id"],
IncludeArchived: includeArchived,
})
@ -82,7 +103,7 @@ func (a *Account) Read(ctx context.Context, w http.ResponseWriter, r *http.Reque
// @Failure 403 {object} weberror.ErrorResponse
// @Failure 500 {object} weberror.ErrorResponse
// @Router /accounts [patch]
func (a *Account) Update(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
func (h *Accounts) Update(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
v, err := webcontext.ContextValues(ctx)
if err != nil {
@ -102,7 +123,7 @@ func (a *Account) Update(ctx context.Context, w http.ResponseWriter, r *http.Req
return web.RespondJsonError(ctx, w, err)
}
err = account.Update(ctx, claims, a.MasterDB, req, v.Now)
err = h.Repository.Update(ctx, claims, req, v.Now)
if err != nil {
cause := errors.Cause(err)
switch cause {

View File

@ -0,0 +1,49 @@
package handlers
import (
"context"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/auth"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/weberror"
"geeks-accelerator/oss/saas-starter-kit/internal/project"
"net/http"
"github.com/pkg/errors"
)
// Example represents the Example API method handler set.
type Example struct {
Project ProjectRepository
// ADD OTHER STATE LIKE THE LOGGER AND CONFIG HERE.
}
// ErrorResponse returns example error messages.
func (h *Example) ErrorResponse(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
v, err := webcontext.ContextValues(ctx)
if err != nil {
return err
}
if qv := r.URL.Query().Get("test-validation-error"); qv != "" {
_, err := h.Project.Create(ctx, auth.Claims{}, project.ProjectCreateRequest{}, v.Now)
return web.RespondJsonError(ctx, w, err)
}
if qv := r.URL.Query().Get("test-web-error"); qv != "" {
terr := errors.New("Some random error")
terr = errors.WithMessage(terr, "Actual error message")
rerr := weberror.NewError(ctx, terr, http.StatusBadRequest).(*weberror.Error)
rerr.Message = "Test Web Error Message"
return web.RespondJsonError(ctx, w, rerr)
}
if qv := r.URL.Query().Get("test-error"); qv != "" {
terr := errors.New("Test error")
terr = errors.WithMessage(terr, "Error message")
return web.RespondJsonError(ctx, w, terr)
}
return nil
}

View File

@ -5,24 +5,35 @@ import (
"net/http"
"strconv"
"strings"
"time"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/auth"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/weberror"
"geeks-accelerator/oss/saas-starter-kit/internal/project"
"github.com/jmoiron/sqlx"
"github.com/pkg/errors"
"gopkg.in/go-playground/validator.v9"
)
// Project represents the Project API method handler set.
type Project struct {
MasterDB *sqlx.DB
type Projects struct {
Repository ProjectRepository
// ADD OTHER STATE LIKE THE LOGGER IF NEEDED.
}
type ProjectRepository interface {
ReadByID(ctx context.Context, claims auth.Claims, id string) (*project.Project, error)
Find(ctx context.Context, claims auth.Claims, req project.ProjectFindRequest) (project.Projects, error)
Read(ctx context.Context, claims auth.Claims, req project.ProjectReadRequest) (*project.Project, error)
Create(ctx context.Context, claims auth.Claims, req project.ProjectCreateRequest, now time.Time) (*project.Project, error)
Update(ctx context.Context, claims auth.Claims, req project.ProjectUpdateRequest, now time.Time) error
Archive(ctx context.Context, claims auth.Claims, req project.ProjectArchiveRequest, now time.Time) error
Delete(ctx context.Context, claims auth.Claims, req project.ProjectDeleteRequest) error
}
// Find godoc
// TODO: Need to implement unittests on projects/find endpoint. There are none.
// @Summary List projects
@ -41,7 +52,7 @@ type Project struct {
// @Failure 403 {object} weberror.ErrorResponse
// @Failure 500 {object} weberror.ErrorResponse
// @Router /projects [get]
func (p *Project) Find(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
func (h *Projects) 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")
@ -108,7 +119,7 @@ func (p *Project) Find(ctx context.Context, w http.ResponseWriter, r *http.Reque
// return web.RespondJsonError(ctx, w, err)
//}
res, err := project.Find(ctx, claims, p.MasterDB, req)
res, err := h.Repository.Find(ctx, claims, req)
if err != nil {
return err
}
@ -134,7 +145,7 @@ func (p *Project) Find(ctx context.Context, w http.ResponseWriter, r *http.Reque
// @Failure 404 {object} weberror.ErrorResponse
// @Failure 500 {object} weberror.ErrorResponse
// @Router /projects/{id} [get]
func (p *Project) Read(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
func (h *Projects) Read(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
claims, ok := ctx.Value(auth.Key).(auth.Claims)
if !ok {
return errors.New("claims missing from context")
@ -151,7 +162,7 @@ func (p *Project) Read(ctx context.Context, w http.ResponseWriter, r *http.Reque
includeArchived = b
}
res, err := project.Read(ctx, claims, p.MasterDB, project.ProjectReadRequest{
res, err := h.Repository.Read(ctx, claims, project.ProjectReadRequest{
ID: params["id"],
IncludeArchived: includeArchived,
})
@ -182,7 +193,7 @@ func (p *Project) Read(ctx context.Context, w http.ResponseWriter, r *http.Reque
// @Failure 404 {object} weberror.ErrorResponse
// @Failure 500 {object} weberror.ErrorResponse
// @Router /projects [post]
func (p *Project) Create(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
func (h *Projects) Create(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
v, err := webcontext.ContextValues(ctx)
if err != nil {
return err
@ -201,7 +212,7 @@ func (p *Project) Create(ctx context.Context, w http.ResponseWriter, r *http.Req
return web.RespondJsonError(ctx, w, err)
}
res, err := project.Create(ctx, claims, p.MasterDB, req, v.Now)
res, err := h.Repository.Create(ctx, claims, req, v.Now)
if err != nil {
cause := errors.Cause(err)
switch cause {
@ -232,7 +243,7 @@ func (p *Project) Create(ctx context.Context, w http.ResponseWriter, r *http.Req
// @Failure 403 {object} weberror.ErrorResponse
// @Failure 500 {object} weberror.ErrorResponse
// @Router /projects [patch]
func (p *Project) Update(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
func (h *Projects) Update(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
v, err := webcontext.ContextValues(ctx)
if err != nil {
return err
@ -251,7 +262,7 @@ func (p *Project) Update(ctx context.Context, w http.ResponseWriter, r *http.Req
return web.RespondJsonError(ctx, w, err)
}
err = project.Update(ctx, claims, p.MasterDB, req, v.Now)
err = h.Repository.Update(ctx, claims, req, v.Now)
if err != nil {
cause := errors.Cause(err)
switch cause {
@ -283,7 +294,7 @@ func (p *Project) Update(ctx context.Context, w http.ResponseWriter, r *http.Req
// @Failure 403 {object} weberror.ErrorResponse
// @Failure 500 {object} weberror.ErrorResponse
// @Router /projects/archive [patch]
func (p *Project) Archive(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
func (h *Projects) Archive(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
v, err := webcontext.ContextValues(ctx)
if err != nil {
return err
@ -302,7 +313,7 @@ func (p *Project) Archive(ctx context.Context, w http.ResponseWriter, r *http.Re
return web.RespondJsonError(ctx, w, err)
}
err = project.Archive(ctx, claims, p.MasterDB, req, v.Now)
err = h.Repository.Archive(ctx, claims, req, v.Now)
if err != nil {
cause := errors.Cause(err)
switch cause {
@ -334,13 +345,13 @@ func (p *Project) Archive(ctx context.Context, w http.ResponseWriter, r *http.Re
// @Failure 403 {object} weberror.ErrorResponse
// @Failure 500 {object} weberror.ErrorResponse
// @Router /projects/{id} [delete]
func (p *Project) Delete(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
func (h *Projects) Delete(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
claims, err := auth.ClaimsFromContext(ctx)
if err != nil {
return err
}
err = project.Delete(ctx, claims, p.MasterDB,
err = h.Repository.Delete(ctx, claims,
project.ProjectDeleteRequest{ID: params["id"]})
if err != nil {
cause := errors.Cause(err)

View File

@ -1,7 +1,6 @@
package handlers
import (
"context"
"log"
"net/http"
"os"
@ -11,92 +10,118 @@ import (
"geeks-accelerator/oss/saas-starter-kit/internal/platform/auth"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/weberror"
_ "geeks-accelerator/oss/saas-starter-kit/internal/platform/web/weberror"
"geeks-accelerator/oss/saas-starter-kit/internal/project"
_ "geeks-accelerator/oss/saas-starter-kit/internal/signup"
"github.com/jmoiron/sqlx"
"github.com/pkg/errors"
"gopkg.in/DataDog/dd-trace-go.v1/contrib/go-redis/redis"
)
type AppContext struct {
Log *log.Logger
Env webcontext.Env
MasterDB *sqlx.DB
Redis *redis.Client
UserRepo UserRepository
UserAccountRepo UserAccountRepository
AccountRepo AccountRepository
AccountPrefRepo AccountPrefRepository
AuthRepo UserAuthRepository
SignupRepo SignupRepository
InviteRepo UserInviteRepository
ProjectRepo ProjectRepository
Authenticator *auth.Authenticator
PreAppMiddleware []web.Middleware
PostAppMiddleware []web.Middleware
}
// API returns a handler for a set of routes.
func API(shutdown chan os.Signal, log *log.Logger, env webcontext.Env, masterDB *sqlx.DB, redis *redis.Client, authenticator *auth.Authenticator, globalMids ...web.Middleware) http.Handler {
func API(shutdown chan os.Signal, appCtx *AppContext) http.Handler {
// Define base middlewares applied to all requests.
middlewares := []web.Middleware{
mid.Trace(), mid.Logger(log), mid.Errors(log, nil), mid.Metrics(), mid.Panics(),
}
// Include the pre middlewares first.
middlewares := appCtx.PreAppMiddleware
// Append any global middlewares if they were included.
if len(globalMids) > 0 {
middlewares = append(middlewares, globalMids...)
// Define app middlewares applied to all requests.
middlewares = append(middlewares,
mid.Trace(),
mid.Logger(appCtx.Log),
mid.Errors(appCtx.Log, nil),
mid.Metrics(),
mid.Panics())
// Append any global middlewares that should be included after the app middlewares.
if len(appCtx.PostAppMiddleware) > 0 {
middlewares = append(middlewares, appCtx.PostAppMiddleware...)
}
// Construct the web.App which holds all routes as well as common Middleware.
app := web.NewApp(shutdown, log, env, middlewares...)
app := web.NewApp(shutdown, appCtx.Log, appCtx.Env, middlewares...)
// Register health check endpoint. This route is not authenticated.
check := Check{
MasterDB: masterDB,
Redis: redis,
MasterDB: appCtx.MasterDB,
Redis: appCtx.Redis,
}
app.Handle("GET", "/v1/health", check.Health)
app.Handle("GET", "/ping", check.Ping)
// Register user management and authentication endpoints.
u := User{
MasterDB: masterDB,
TokenGenerator: authenticator,
// Register example endpoints.
ex := Example{
Project: appCtx.ProjectRepo,
}
app.Handle("GET", "/v1/users", u.Find, mid.AuthenticateHeader(authenticator))
app.Handle("POST", "/v1/users", u.Create, mid.AuthenticateHeader(authenticator), mid.HasRole(auth.RoleAdmin))
app.Handle("GET", "/v1/users/:id", u.Read, mid.AuthenticateHeader(authenticator))
app.Handle("PATCH", "/v1/users", u.Update, mid.AuthenticateHeader(authenticator))
app.Handle("PATCH", "/v1/users/password", u.UpdatePassword, mid.AuthenticateHeader(authenticator))
app.Handle("PATCH", "/v1/users/archive", u.Archive, mid.AuthenticateHeader(authenticator), mid.HasRole(auth.RoleAdmin))
app.Handle("DELETE", "/v1/users/:id", u.Delete, mid.AuthenticateHeader(authenticator), mid.HasRole(auth.RoleAdmin))
app.Handle("PATCH", "/v1/users/switch-account/:account_id", u.SwitchAccount, mid.AuthenticateHeader(authenticator))
app.Handle("GET", "/v1/examples/error-response", ex.ErrorResponse)
// Register user management and authentication endpoints.
u := Users{
UserRepo: appCtx.UserRepo,
AuthRepo: appCtx.AuthRepo,
}
app.Handle("GET", "/v1/users", u.Find, mid.AuthenticateHeader(appCtx.Authenticator))
app.Handle("POST", "/v1/users", u.Create, mid.AuthenticateHeader(appCtx.Authenticator), mid.HasRole(auth.RoleAdmin))
app.Handle("GET", "/v1/users/:id", u.Read, mid.AuthenticateHeader(appCtx.Authenticator))
app.Handle("PATCH", "/v1/users", u.Update, mid.AuthenticateHeader(appCtx.Authenticator))
app.Handle("PATCH", "/v1/users/password", u.UpdatePassword, mid.AuthenticateHeader(appCtx.Authenticator))
app.Handle("PATCH", "/v1/users/archive", u.Archive, mid.AuthenticateHeader(appCtx.Authenticator), mid.HasRole(auth.RoleAdmin))
app.Handle("DELETE", "/v1/users/:id", u.Delete, mid.AuthenticateHeader(appCtx.Authenticator), mid.HasRole(auth.RoleAdmin))
app.Handle("PATCH", "/v1/users/switch-account/:account_id", u.SwitchAccount, mid.AuthenticateHeader(appCtx.Authenticator))
// This route is not authenticated
app.Handle("POST", "/v1/oauth/token", u.Token)
// Register user account management endpoints.
ua := UserAccount{
MasterDB: masterDB,
Repository: appCtx.UserAccountRepo,
}
app.Handle("GET", "/v1/user_accounts", ua.Find, mid.AuthenticateHeader(authenticator))
app.Handle("POST", "/v1/user_accounts", ua.Create, mid.AuthenticateHeader(authenticator), mid.HasRole(auth.RoleAdmin))
app.Handle("GET", "/v1/user_accounts/:user_id/:account_id", ua.Read, mid.AuthenticateHeader(authenticator))
app.Handle("PATCH", "/v1/user_accounts", ua.Update, mid.AuthenticateHeader(authenticator))
app.Handle("PATCH", "/v1/user_accounts/archive", ua.Archive, mid.AuthenticateHeader(authenticator), mid.HasRole(auth.RoleAdmin))
app.Handle("DELETE", "/v1/user_accounts", ua.Delete, mid.AuthenticateHeader(authenticator), mid.HasRole(auth.RoleAdmin))
app.Handle("GET", "/v1/user_accounts", ua.Find, mid.AuthenticateHeader(appCtx.Authenticator))
app.Handle("POST", "/v1/user_accounts", ua.Create, mid.AuthenticateHeader(appCtx.Authenticator), mid.HasRole(auth.RoleAdmin))
app.Handle("GET", "/v1/user_accounts/:user_id/:account_id", ua.Read, mid.AuthenticateHeader(appCtx.Authenticator))
app.Handle("PATCH", "/v1/user_accounts", ua.Update, mid.AuthenticateHeader(appCtx.Authenticator))
app.Handle("PATCH", "/v1/user_accounts/archive", ua.Archive, mid.AuthenticateHeader(appCtx.Authenticator), mid.HasRole(auth.RoleAdmin))
app.Handle("DELETE", "/v1/user_accounts", ua.Delete, mid.AuthenticateHeader(appCtx.Authenticator), mid.HasRole(auth.RoleAdmin))
// Register account endpoints.
a := Account{
MasterDB: masterDB,
a := Accounts{
Repository: appCtx.AccountRepo,
}
app.Handle("GET", "/v1/accounts/:id", a.Read, mid.AuthenticateHeader(authenticator))
app.Handle("PATCH", "/v1/accounts", a.Update, mid.AuthenticateHeader(authenticator), mid.HasRole(auth.RoleAdmin))
app.Handle("GET", "/v1/accounts/:id", a.Read, mid.AuthenticateHeader(appCtx.Authenticator))
app.Handle("PATCH", "/v1/accounts", a.Update, mid.AuthenticateHeader(appCtx.Authenticator), mid.HasRole(auth.RoleAdmin))
// Register signup endpoints.
s := Signup{
MasterDB: masterDB,
Repository: appCtx.SignupRepo,
}
app.Handle("POST", "/v1/signup", s.Signup)
// Register project.
p := Project{
MasterDB: masterDB,
p := Projects{
Repository: appCtx.ProjectRepo,
}
app.Handle("GET", "/v1/projects", p.Find, mid.AuthenticateHeader(authenticator))
app.Handle("POST", "/v1/projects", p.Create, mid.AuthenticateHeader(authenticator), mid.HasRole(auth.RoleAdmin))
app.Handle("GET", "/v1/projects/:id", p.Read, mid.AuthenticateHeader(authenticator))
app.Handle("PATCH", "/v1/projects", p.Update, mid.AuthenticateHeader(authenticator), mid.HasRole(auth.RoleAdmin))
app.Handle("PATCH", "/v1/projects/archive", p.Archive, mid.AuthenticateHeader(authenticator), mid.HasRole(auth.RoleAdmin))
app.Handle("DELETE", "/v1/projects/:id", p.Delete, mid.AuthenticateHeader(authenticator), mid.HasRole(auth.RoleAdmin))
app.Handle("GET", "/v1/examples/error-response", ExampleErrorResponse)
app.Handle("GET", "/v1/projects", p.Find, mid.AuthenticateHeader(appCtx.Authenticator))
app.Handle("POST", "/v1/projects", p.Create, mid.AuthenticateHeader(appCtx.Authenticator), mid.HasRole(auth.RoleAdmin))
app.Handle("GET", "/v1/projects/:id", p.Read, mid.AuthenticateHeader(appCtx.Authenticator))
app.Handle("PATCH", "/v1/projects", p.Update, mid.AuthenticateHeader(appCtx.Authenticator), mid.HasRole(auth.RoleAdmin))
app.Handle("PATCH", "/v1/projects/archive", p.Archive, mid.AuthenticateHeader(appCtx.Authenticator), mid.HasRole(auth.RoleAdmin))
app.Handle("DELETE", "/v1/projects/:id", p.Delete, mid.AuthenticateHeader(appCtx.Authenticator), mid.HasRole(auth.RoleAdmin))
// Register swagger documentation.
// TODO: Add authentication. Current authenticator requires an Authorization header
@ -107,36 +132,6 @@ func API(shutdown chan os.Signal, log *log.Logger, env webcontext.Env, masterDB
return app
}
// ExampleErrorResponse returns example error messages.
func ExampleErrorResponse(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
v, err := webcontext.ContextValues(ctx)
if err != nil {
return err
}
if qv := r.URL.Query().Get("test-validation-error"); qv != "" {
_, err := project.Create(ctx, auth.Claims{}, nil, project.ProjectCreateRequest{}, v.Now)
return web.RespondJsonError(ctx, w, err)
}
if qv := r.URL.Query().Get("test-web-error"); qv != "" {
terr := errors.New("Some random error")
terr = errors.WithMessage(terr, "Actual error message")
rerr := weberror.NewError(ctx, terr, http.StatusBadRequest).(*weberror.Error)
rerr.Message = "Test Web Error Message"
return web.RespondJsonError(ctx, w, rerr)
}
if qv := r.URL.Query().Get("test-error"); qv != "" {
terr := errors.New("Test error")
terr = errors.WithMessage(terr, "Error message")
return web.RespondJsonError(ctx, w, terr)
}
return nil
}
// Types godoc
// @Summary List of types.
// @Param data body weberror.FieldError false "Field Error"

View File

@ -3,6 +3,7 @@ package handlers
import (
"context"
"net/http"
"time"
"geeks-accelerator/oss/saas-starter-kit/internal/account"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/auth"
@ -10,18 +11,22 @@ import (
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/weberror"
"geeks-accelerator/oss/saas-starter-kit/internal/signup"
"github.com/jmoiron/sqlx"
"github.com/pkg/errors"
"gopkg.in/go-playground/validator.v9"
)
// Signup represents the Signup API method handler set.
type Signup struct {
MasterDB *sqlx.DB
Repository SignupRepository
// ADD OTHER STATE LIKE THE LOGGER AND CONFIG HERE.
}
type SignupRepository interface {
Signup(ctx context.Context, claims auth.Claims, req signup.SignupRequest, now time.Time) (*signup.SignupResult, error)
}
// Signup godoc
// @Summary Signup handles new account creation.
// @Description Signup creates a new account and user in the system.
@ -33,7 +38,7 @@ type Signup struct {
// @Failure 400 {object} weberror.ErrorResponse
// @Failure 500 {object} weberror.ErrorResponse
// @Router /signup [post]
func (c *Signup) Signup(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
func (h *Signup) Signup(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
v, err := webcontext.ContextValues(ctx)
if err != nil {
return err
@ -50,7 +55,7 @@ func (c *Signup) Signup(ctx context.Context, w http.ResponseWriter, r *http.Requ
return web.RespondJsonError(ctx, w, err)
}
res, err := signup.Signup(ctx, claims, c.MasterDB, req, v.Now)
res, err := h.Repository.Signup(ctx, claims, req, v.Now)
if err != nil {
switch errors.Cause(err) {
case account.ErrForbidden:

View File

@ -13,8 +13,8 @@ import (
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/weberror"
"geeks-accelerator/oss/saas-starter-kit/internal/user"
"geeks-accelerator/oss/saas-starter-kit/internal/user_auth"
"github.com/gorilla/schema"
"github.com/jmoiron/sqlx"
"github.com/pkg/errors"
"gopkg.in/go-playground/validator.v9"
)
@ -23,13 +23,36 @@ import (
var sessionTtl = time.Hour * 24
// User represents the User API method handler set.
type User struct {
MasterDB *sqlx.DB
TokenGenerator user_auth.TokenGenerator
type Users struct {
AuthRepo UserAuthRepository
UserRepo UserRepository
// ADD OTHER STATE LIKE THE LOGGER AND CONFIG HERE.
}
type UserAuthRepository interface {
SwitchAccount(ctx context.Context, claims auth.Claims, req user_auth.SwitchAccountRequest, expires time.Duration,
now time.Time, scopes ...string) (user_auth.Token, error)
Authenticate(ctx context.Context, req user_auth.AuthenticateRequest, expires time.Duration, now time.Time, scopes ...string) (user_auth.Token, error)
VirtualLogin(ctx context.Context, claims auth.Claims, req user_auth.VirtualLoginRequest,
expires time.Duration, now time.Time, scopes ...string) (user_auth.Token, error)
VirtualLogout(ctx context.Context, claims auth.Claims, expires time.Duration, now time.Time, scopes ...string) (user_auth.Token, error)
}
type UserRepository interface {
Find(ctx context.Context, claims auth.Claims, req user.UserFindRequest) (user.Users, error)
//FindByAccount(ctx context.Context, claims auth.Claims, req user.UserFindByAccountRequest) (user.Users, error)
Read(ctx context.Context, claims auth.Claims, req user.UserReadRequest) (*user.User, error)
ReadByID(ctx context.Context, claims auth.Claims, id string) (*user.User, error)
Create(ctx context.Context, claims auth.Claims, req user.UserCreateRequest, now time.Time) (*user.User, error)
Update(ctx context.Context, claims auth.Claims, req user.UserUpdateRequest, now time.Time) error
UpdatePassword(ctx context.Context, claims auth.Claims, req user.UserUpdatePasswordRequest, now time.Time) error
Archive(ctx context.Context, claims auth.Claims, req user.UserArchiveRequest, now time.Time) error
Restore(ctx context.Context, claims auth.Claims, req user.UserRestoreRequest, now time.Time) error
Delete(ctx context.Context, claims auth.Claims, req user.UserDeleteRequest) error
ResetPassword(ctx context.Context, req user.UserResetPasswordRequest, now time.Time) (string, error)
ResetConfirm(ctx context.Context, req user.UserResetConfirmRequest, now time.Time) (*user.User, error)
}
// Find godoc
// TODO: Need to implement unittests on users/find endpoint. There are none.
// @Summary List users
@ -47,7 +70,7 @@ type User struct {
// @Failure 400 {object} weberror.ErrorResponse
// @Failure 500 {object} weberror.ErrorResponse
// @Router /users [get]
func (u *User) Find(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
func (h *Users) 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")
@ -114,7 +137,7 @@ func (u *User) Find(ctx context.Context, w http.ResponseWriter, r *http.Request,
// return web.RespondJsonError(ctx, w, err)
//}
res, err := user.Find(ctx, claims, u.MasterDB, req)
res, err := h.UserRepo.Find(ctx, claims, req)
if err != nil {
return err
}
@ -140,7 +163,7 @@ func (u *User) Find(ctx context.Context, w http.ResponseWriter, r *http.Request,
// @Failure 404 {object} weberror.ErrorResponse
// @Failure 500 {object} weberror.ErrorResponse
// @Router /users/{id} [get]
func (u *User) Read(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
func (h *Users) Read(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
claims, ok := ctx.Value(auth.Key).(auth.Claims)
if !ok {
return errors.New("claims missing from context")
@ -157,7 +180,7 @@ func (u *User) Read(ctx context.Context, w http.ResponseWriter, r *http.Request,
includeArchived = b
}
res, err := user.Read(ctx, claims, u.MasterDB, user.UserReadRequest{
res, err := h.UserRepo.Read(ctx, claims, user.UserReadRequest{
ID: params["id"],
IncludeArchived: includeArchived,
})
@ -187,7 +210,7 @@ func (u *User) Read(ctx context.Context, w http.ResponseWriter, r *http.Request,
// @Failure 403 {object} weberror.ErrorResponse
// @Failure 500 {object} weberror.ErrorResponse
// @Router /users [post]
func (u *User) Create(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
func (h *Users) Create(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
v, err := webcontext.ContextValues(ctx)
if err != nil {
return err
@ -206,7 +229,7 @@ func (u *User) Create(ctx context.Context, w http.ResponseWriter, r *http.Reques
return web.RespondJsonError(ctx, w, err)
}
res, err := user.Create(ctx, claims, u.MasterDB, req, v.Now)
usr, err := h.UserRepo.Create(ctx, claims, req, v.Now)
if err != nil {
cause := errors.Cause(err)
switch cause {
@ -222,7 +245,7 @@ func (u *User) Create(ctx context.Context, w http.ResponseWriter, r *http.Reques
}
}
return web.RespondJson(ctx, w, res.Response(ctx), http.StatusCreated)
return web.RespondJson(ctx, w, usr.Response(ctx), http.StatusCreated)
}
// Read godoc
@ -238,7 +261,7 @@ func (u *User) Create(ctx context.Context, w http.ResponseWriter, r *http.Reques
// @Failure 403 {object} weberror.ErrorResponse
// @Failure 500 {object} weberror.ErrorResponse
// @Router /users [patch]
func (u *User) Update(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
func (h *Users) Update(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
v, err := webcontext.ContextValues(ctx)
if err != nil {
return err
@ -257,7 +280,7 @@ func (u *User) Update(ctx context.Context, w http.ResponseWriter, r *http.Reques
return web.RespondJsonError(ctx, w, err)
}
err = user.Update(ctx, claims, u.MasterDB, req, v.Now)
err = h.UserRepo.Update(ctx, claims, req, v.Now)
if err != nil {
cause := errors.Cause(err)
switch cause {
@ -289,7 +312,7 @@ func (u *User) Update(ctx context.Context, w http.ResponseWriter, r *http.Reques
// @Failure 403 {object} weberror.ErrorResponse
// @Failure 500 {object} weberror.ErrorResponse
// @Router /users/password [patch]
func (u *User) UpdatePassword(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
func (h *Users) UpdatePassword(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
v, err := webcontext.ContextValues(ctx)
if err != nil {
return err
@ -308,7 +331,7 @@ func (u *User) UpdatePassword(ctx context.Context, w http.ResponseWriter, r *htt
return web.RespondJsonError(ctx, w, err)
}
err = user.UpdatePassword(ctx, claims, u.MasterDB, req, v.Now)
err = h.UserRepo.UpdatePassword(ctx, claims, req, v.Now)
if err != nil {
cause := errors.Cause(err)
switch cause {
@ -342,7 +365,7 @@ func (u *User) UpdatePassword(ctx context.Context, w http.ResponseWriter, r *htt
// @Failure 403 {object} weberror.ErrorResponse
// @Failure 500 {object} weberror.ErrorResponse
// @Router /users/archive [patch]
func (u *User) Archive(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
func (h *Users) Archive(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
v, err := webcontext.ContextValues(ctx)
if err != nil {
return err
@ -361,7 +384,7 @@ func (u *User) Archive(ctx context.Context, w http.ResponseWriter, r *http.Reque
return web.RespondJsonError(ctx, w, err)
}
err = user.Archive(ctx, claims, u.MasterDB, req, v.Now)
err = h.UserRepo.Archive(ctx, claims, req, v.Now)
if err != nil {
cause := errors.Cause(err)
switch cause {
@ -393,13 +416,13 @@ func (u *User) Archive(ctx context.Context, w http.ResponseWriter, r *http.Reque
// @Failure 403 {object} weberror.ErrorResponse
// @Failure 500 {object} weberror.ErrorResponse
// @Router /users/{id} [delete]
func (u *User) Delete(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
func (h *Users) Delete(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
claims, err := auth.ClaimsFromContext(ctx)
if err != nil {
return err
}
err = user.Delete(ctx, claims, u.MasterDB,
err = h.UserRepo.Delete(ctx, claims,
user.UserDeleteRequest{ID: params["id"]})
if err != nil {
cause := errors.Cause(err)
@ -432,7 +455,7 @@ func (u *User) Delete(ctx context.Context, w http.ResponseWriter, r *http.Reques
// @Failure 401 {object} weberror.ErrorResponse
// @Failure 500 {object} weberror.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 (h *Users) SwitchAccount(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
v, err := webcontext.ContextValues(ctx)
if err != nil {
return err
@ -443,7 +466,7 @@ func (u *User) SwitchAccount(ctx context.Context, w http.ResponseWriter, r *http
return err
}
tkn, err := user_auth.SwitchAccount(ctx, u.MasterDB, u.TokenGenerator, claims, user_auth.SwitchAccountRequest{
tkn, err := h.AuthRepo.SwitchAccount(ctx, claims, user_auth.SwitchAccountRequest{
AccountID: params["account_id"],
}, sessionTtl, v.Now)
if err != nil {
@ -479,7 +502,7 @@ func (u *User) SwitchAccount(ctx context.Context, w http.ResponseWriter, r *http
// @Failure 401 {object} weberror.ErrorResponse
// @Failure 500 {object} weberror.ErrorResponse
// @Router /oauth/token [post]
func (u *User) Token(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
func (h *Users) Token(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
v, err := webcontext.ContextValues(ctx)
if err != nil {
return err
@ -534,7 +557,7 @@ func (u *User) Token(ctx context.Context, w http.ResponseWriter, r *http.Request
scopes = strings.Split(qv, ",")
}
tkn, err := user_auth.Authenticate(ctx, u.MasterDB, u.TokenGenerator, authReq, sessionTtl, v.Now, scopes...)
tkn, err := h.AuthRepo.Authenticate(ctx, authReq, sessionTtl, v.Now, scopes...)
if err != nil {
cause := errors.Cause(err)
switch cause {

View File

@ -5,24 +5,44 @@ import (
"net/http"
"strconv"
"strings"
"time"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/auth"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/weberror"
"geeks-accelerator/oss/saas-starter-kit/internal/user_account"
"github.com/jmoiron/sqlx"
"geeks-accelerator/oss/saas-starter-kit/internal/user_account/invite"
"github.com/pkg/errors"
"gopkg.in/go-playground/validator.v9"
)
// UserAccount represents the UserAccount API method handler set.
type UserAccount struct {
MasterDB *sqlx.DB
UserInvite UserInviteRepository
Repository UserAccountRepository
// ADD OTHER STATE LIKE THE LOGGER AND CONFIG HERE.
}
type UserAccountRepository interface {
Find(ctx context.Context, claims auth.Claims, req user_account.UserAccountFindRequest) (user_account.UserAccounts, error)
FindByUserID(ctx context.Context, claims auth.Claims, userID string, includedArchived bool) (user_account.UserAccounts, error)
UserFindByAccount(ctx context.Context, claims auth.Claims, req user_account.UserFindByAccountRequest) (user_account.Users, error)
Create(ctx context.Context, claims auth.Claims, req user_account.UserAccountCreateRequest, now time.Time) (*user_account.UserAccount, error)
Read(ctx context.Context, claims auth.Claims, req user_account.UserAccountReadRequest) (*user_account.UserAccount, error)
Update(ctx context.Context, claims auth.Claims, req user_account.UserAccountUpdateRequest, now time.Time) error
Archive(ctx context.Context, claims auth.Claims, req user_account.UserAccountArchiveRequest, now time.Time) error
Delete(ctx context.Context, claims auth.Claims, req user_account.UserAccountDeleteRequest) error
}
type UserInviteRepository interface {
SendUserInvites(ctx context.Context, claims auth.Claims, req invite.SendUserInvitesRequest, now time.Time) ([]string, error)
AcceptInvite(ctx context.Context, req invite.AcceptInviteRequest, now time.Time) (*user_account.UserAccount, error)
AcceptInviteUser(ctx context.Context, req invite.AcceptInviteUserRequest, now time.Time) (*user_account.UserAccount, error)
}
// Find godoc
// TODO: Need to implement unittests on user_accounts/find endpoint. There are none.
// @Summary List user accounts
@ -41,7 +61,7 @@ type UserAccount struct {
// @Failure 403 {object} weberror.ErrorResponse
// @Failure 500 {object} weberror.ErrorResponse
// @Router /user_accounts [get]
func (u *UserAccount) Find(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
func (h *UserAccount) Find(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
claims, ok := ctx.Value(auth.Key).(auth.Claims)
if !ok {
return errors.New("claims missing from context")
@ -108,7 +128,7 @@ func (u *UserAccount) Find(ctx context.Context, w http.ResponseWriter, r *http.R
// return web.RespondJsonError(ctx, w, err)
//}
res, err := user_account.Find(ctx, claims, u.MasterDB, req)
res, err := h.Repository.Find(ctx, claims, req)
if err != nil {
return err
}
@ -134,7 +154,7 @@ func (u *UserAccount) Find(ctx context.Context, w http.ResponseWriter, r *http.R
// @Failure 404 {object} weberror.ErrorResponse
// @Failure 500 {object} weberror.ErrorResponse
// @Router /user_accounts/{user_id}/{account_id} [get]
func (u *UserAccount) Read(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
func (h *UserAccount) Read(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
claims, ok := ctx.Value(auth.Key).(auth.Claims)
if !ok {
return errors.New("claims missing from context")
@ -151,7 +171,7 @@ func (u *UserAccount) Read(ctx context.Context, w http.ResponseWriter, r *http.R
includeArchived = b
}
res, err := user_account.Read(ctx, claims, u.MasterDB, user_account.UserAccountReadRequest{
res, err := h.Repository.Read(ctx, claims, user_account.UserAccountReadRequest{
UserID: params["user_id"],
AccountID: params["account_id"],
IncludeArchived: includeArchived,
@ -183,7 +203,7 @@ func (u *UserAccount) Read(ctx context.Context, w http.ResponseWriter, r *http.R
// @Failure 404 {object} weberror.ErrorResponse
// @Failure 500 {object} weberror.ErrorResponse
// @Router /user_accounts [post]
func (u *UserAccount) Create(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
func (h *UserAccount) Create(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
v, err := webcontext.ContextValues(ctx)
if err != nil {
return err
@ -202,7 +222,7 @@ func (u *UserAccount) Create(ctx context.Context, w http.ResponseWriter, r *http
return web.RespondJsonError(ctx, w, err)
}
res, err := user_account.Create(ctx, claims, u.MasterDB, req, v.Now)
res, err := h.Repository.Create(ctx, claims, req, v.Now)
if err != nil {
cause := errors.Cause(err)
switch cause {
@ -234,7 +254,7 @@ func (u *UserAccount) Create(ctx context.Context, w http.ResponseWriter, r *http
// @Failure 403 {object} weberror.ErrorResponse
// @Failure 500 {object} weberror.ErrorResponse
// @Router /user_accounts [patch]
func (u *UserAccount) Update(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
func (h *UserAccount) Update(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
v, err := webcontext.ContextValues(ctx)
if err != nil {
return err
@ -253,7 +273,7 @@ func (u *UserAccount) Update(ctx context.Context, w http.ResponseWriter, r *http
return web.RespondJsonError(ctx, w, err)
}
err = user_account.Update(ctx, claims, u.MasterDB, req, v.Now)
err = h.Repository.Update(ctx, claims, req, v.Now)
if err != nil {
cause := errors.Cause(err)
switch cause {
@ -285,7 +305,7 @@ func (u *UserAccount) Update(ctx context.Context, w http.ResponseWriter, r *http
// @Failure 403 {object} weberror.ErrorResponse
// @Failure 500 {object} weberror.ErrorResponse
// @Router /user_accounts/archive [patch]
func (u *UserAccount) Archive(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
func (h *UserAccount) Archive(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
v, err := webcontext.ContextValues(ctx)
if err != nil {
return err
@ -304,7 +324,7 @@ func (u *UserAccount) Archive(ctx context.Context, w http.ResponseWriter, r *htt
return web.RespondJsonError(ctx, w, err)
}
err = user_account.Archive(ctx, claims, u.MasterDB, req, v.Now)
err = h.Repository.Archive(ctx, claims, req, v.Now)
if err != nil {
cause := errors.Cause(err)
switch cause {
@ -336,7 +356,7 @@ func (u *UserAccount) Archive(ctx context.Context, w http.ResponseWriter, r *htt
// @Failure 403 {object} weberror.ErrorResponse
// @Failure 500 {object} weberror.ErrorResponse
// @Router /user_accounts [delete]
func (u *UserAccount) Delete(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
func (h *UserAccount) Delete(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
claims, err := auth.ClaimsFromContext(ctx)
if err != nil {
return err
@ -350,7 +370,7 @@ func (u *UserAccount) Delete(ctx context.Context, w http.ResponseWriter, r *http
return web.RespondJsonError(ctx, w, err)
}
err = user_account.Delete(ctx, claims, u.MasterDB, req)
err = h.Repository.Delete(ctx, claims, req)
if err != nil {
cause := errors.Cause(err)
switch cause {

View File

@ -6,7 +6,6 @@ import (
"encoding/json"
"expvar"
"fmt"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext"
"log"
"net"
"net/http"
@ -21,18 +20,31 @@ import (
"geeks-accelerator/oss/saas-starter-kit/cmd/web-api/docs"
"geeks-accelerator/oss/saas-starter-kit/cmd/web-api/handlers"
"geeks-accelerator/oss/saas-starter-kit/internal/account"
"geeks-accelerator/oss/saas-starter-kit/internal/account/account_preference"
"geeks-accelerator/oss/saas-starter-kit/internal/mid"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/auth"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/devops"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/flag"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/notify"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext"
"geeks-accelerator/oss/saas-starter-kit/internal/project"
"geeks-accelerator/oss/saas-starter-kit/internal/project_route"
"geeks-accelerator/oss/saas-starter-kit/internal/signup"
"geeks-accelerator/oss/saas-starter-kit/internal/user"
"geeks-accelerator/oss/saas-starter-kit/internal/user_account"
"geeks-accelerator/oss/saas-starter-kit/internal/user_account/invite"
"geeks-accelerator/oss/saas-starter-kit/internal/user_auth"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/ec2metadata"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/go-redis/redis"
"github.com/gorilla/securecookie"
"github.com/kelseyhightower/envconfig"
"github.com/lib/pq"
"github.com/pkg/errors"
"golang.org/x/crypto/acme"
"golang.org/x/crypto/acme/autocert"
awstrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/aws/aws-sdk-go/aws"
@ -66,10 +78,9 @@ func main() {
// =========================================================================
// Logging
log.SetFlags(log.LstdFlags|log.Lmicroseconds|log.Lshortfile)
log.SetPrefix(service+" : ")
log := log.New(os.Stdout, log.Prefix() , log.Flags())
log.SetFlags(log.LstdFlags | log.Lmicroseconds | log.Lshortfile)
log.SetPrefix(service + " : ")
log := log.New(os.Stdout, log.Prefix(), log.Flags())
// =========================================================================
// Configuration
@ -87,16 +98,21 @@ func main() {
DisableHTTP2 bool `default:"false" envconfig:"DISABLE_HTTP2"`
}
Service struct {
Name string `default:"web-api" envconfig:"NAME"`
Project string `default:"" envconfig:"PROJECT"`
Name string `default:"web-api" envconfig:"SERVICE_NAME"`
BaseUrl string `default:"" envconfig:"BASE_URL" example:"http://api.example.saasstartupkit.com"`
HostNames []string `envconfig:"HOST_NAMES" example:"alternative-subdomain.example.saasstartupkit.com"`
EnableHTTPS bool `default:"false" envconfig:"ENABLE_HTTPS"`
TemplateDir string `default:"./templates" envconfig:"TEMPLATE_DIR"`
WebAppBaseUrl string `default:"http://127.0.0.1:3000" envconfig:"WEB_APP_BASE_URL" example:"www.example.saasstartupkit.com"`
DebugHost string `default:"0.0.0.0:4000" envconfig:"DEBUG_HOST"`
ShutdownTimeout time.Duration `default:"5s" envconfig:"SHUTDOWN_TIMEOUT"`
}
Project struct {
Name string `default:"" envconfig:"PROJECT_NAME"`
SharedTemplateDir string `default:"../../resources/templates/shared" envconfig:"SHARED_TEMPLATE_DIR"`
SharedSecretKey string `default:"" envconfig:"SHARED_SECRET_KEY"`
EmailSender string `default:"test@example.saasstartupkit.com" envconfig:"EMAIL_SENDER"`
WebAppBaseUrl string `default:"http://127.0.0.1:3000" envconfig:"WEB_APP_BASE_URL" example:"www.example.saasstartupkit.com"`
}
Redis struct {
Host string `default:":6379" envconfig:"HOST"`
DB int `default:"1" envconfig:"DB"`
@ -185,10 +201,10 @@ func main() {
// deployments and distributed to each instance of the service running.
if cfg.Aws.SecretsManagerConfigPrefix == "" {
var pts []string
if cfg.Service.Project != "" {
pts = append(pts, cfg.Service.Project)
if cfg.Project.Name != "" {
pts = append(pts, cfg.Project.Name)
}
pts = append(pts, cfg.Env, cfg.Service.Name)
pts = append(pts, cfg.Env)
cfg.Aws.SecretsManagerConfigPrefix = filepath.Join(pts...)
}
@ -276,6 +292,37 @@ func main() {
awsSession = awstrace.WrapSession(awsSession)
}
// =========================================================================
// Shared Secret Key used for encrypting sessions and links.
// Set the secret key if not provided in the config.
if cfg.Project.SharedSecretKey == "" {
// AWS secrets manager ID for storing the session key. This is optional and only will be used
// if a valid AWS session is provided.
secretID := filepath.Join(cfg.Aws.SecretsManagerConfigPrefix, "SharedSecretKey")
// If AWS is enabled, check the Secrets Manager for the session key.
if awsSession != nil {
cfg.Project.SharedSecretKey, err = devops.SecretManagerGetString(awsSession, secretID)
if err != nil && errors.Cause(err) != devops.ErrSecreteNotFound {
log.Fatalf("main : Session : %+v", err)
}
}
// If the session key is still empty, generate a new key.
if cfg.Project.SharedSecretKey == "" {
cfg.Project.SharedSecretKey = string(securecookie.GenerateRandomKey(32))
if awsSession != nil {
err = devops.SecretManagerPutString(awsSession, secretID, cfg.Project.SharedSecretKey)
if err != nil {
log.Fatalf("main : Session : %+v", err)
}
}
}
}
// =========================================================================
// Start Redis
// Ensure the eviction policy on the redis cluster is set correctly.
@ -346,6 +393,31 @@ func main() {
}
defer masterDb.Close()
// =========================================================================
// Notify Email
var notifyEmail notify.Email
if awsSession != nil {
// Send emails with AWS SES. Alternative to use SMTP with notify.NewEmailSmtp.
notifyEmail, err = notify.NewEmailAws(awsSession, cfg.Project.SharedTemplateDir, cfg.Project.EmailSender)
if err != nil {
log.Fatalf("main : Notify Email : %+v", err)
}
err = notifyEmail.Verify()
if err != nil {
switch errors.Cause(err) {
case notify.ErrAwsSesIdentityNotVerified:
log.Printf("main : Notify Email : %s\n", err)
case notify.ErrAwsSesSendingDisabled:
log.Printf("main : Notify Email : %s\n", err)
default:
log.Fatalf("main : Notify Email Verify : %+v", err)
}
}
} else {
notifyEmail = notify.NewEmailDisabled()
}
// =========================================================================
// Init new Authenticator
var authenticator *auth.Authenticator
@ -360,11 +432,41 @@ func main() {
}
// =========================================================================
// Load middlewares that need to be configured specific for the service.
var serviceMiddlewares = []web.Middleware{
mid.Translator(webcontext.UniversalTranslator()),
// Init repositories and AppContext
projectRoute, err := project_route.New(cfg.Service.BaseUrl, cfg.Project.WebAppBaseUrl)
if err != nil {
log.Fatalf("main : project routes : %s: %+v", cfg.Service.BaseUrl, err)
}
usrRepo := user.NewRepository(masterDb, projectRoute.UserResetPassword, notifyEmail, cfg.Project.SharedSecretKey)
usrAccRepo := user_account.NewRepository(masterDb)
accRepo := account.NewRepository(masterDb)
accPrefRepo := account_preference.NewRepository(masterDb)
authRepo := user_auth.NewRepository(masterDb, authenticator, usrRepo, usrAccRepo, accPrefRepo)
signupRepo := signup.NewRepository(masterDb, usrRepo, usrAccRepo, accRepo)
inviteRepo := invite.NewRepository(masterDb, usrRepo, usrAccRepo, accRepo, projectRoute.UserInviteAccept, notifyEmail, cfg.Project.SharedSecretKey)
prjRepo := project.NewRepository(masterDb)
appCtx := &handlers.AppContext{
Log: log,
Env: cfg.Env,
MasterDB: masterDb,
Redis: redisClient,
UserRepo: usrRepo,
UserAccountRepo: usrAccRepo,
AccountRepo: accRepo,
AccountPrefRepo: accPrefRepo,
AuthRepo: authRepo,
SignupRepo: signupRepo,
InviteRepo: inviteRepo,
ProjectRepo: prjRepo,
Authenticator: authenticator,
}
// =========================================================================
// Load middlewares that need to be configured specific for the service.
// Init redirect middleware to ensure all requests go to the primary domain contained in the base URL.
if primaryServiceHost != "127.0.0.1" && primaryServiceHost != "localhost" {
redirect := mid.DomainNameRedirect(mid.DomainNameRedirectConfig{
@ -380,9 +482,12 @@ func main() {
DomainName: primaryServiceHost,
HTTPSEnabled: cfg.Service.EnableHTTPS,
})
serviceMiddlewares = append(serviceMiddlewares, redirect)
appCtx.PostAppMiddleware = append(appCtx.PostAppMiddleware, redirect)
}
// Add the translator middleware for localization.
appCtx.PostAppMiddleware = append(appCtx.PostAppMiddleware, mid.Translator(webcontext.UniversalTranslator()))
// =========================================================================
// Start Tracing Support
th := fmt.Sprintf("%s:%d", cfg.Trace.Host, cfg.Trace.Port)
@ -443,7 +548,7 @@ func main() {
if cfg.HTTP.Host != "" {
api := http.Server{
Addr: cfg.HTTP.Host,
Handler: handlers.API(shutdown, log, cfg.Env, masterDb, redisClient, authenticator, serviceMiddlewares...),
Handler: handlers.API(shutdown, appCtx),
ReadTimeout: cfg.HTTP.ReadTimeout,
WriteTimeout: cfg.HTTP.WriteTimeout,
MaxHeaderBytes: 1 << 20,
@ -460,7 +565,7 @@ func main() {
if cfg.HTTPS.Host != "" {
api := http.Server{
Addr: cfg.HTTPS.Host,
Handler: handlers.API(shutdown, log, cfg.Env, masterDb, redisClient, authenticator, serviceMiddlewares...),
Handler: handlers.API(shutdown, appCtx),
ReadTimeout: cfg.HTTPS.ReadTimeout,
WriteTimeout: cfg.HTTPS.WriteTimeout,
MaxHeaderBytes: 1 << 20,

View File

@ -13,6 +13,7 @@ import (
"geeks-accelerator/oss/saas-starter-kit/internal/platform/tests"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/weberror"
"github.com/pborman/uuid"
)

View File

@ -27,7 +27,7 @@ func mockProjectCreateRequest(accountID string) project.ProjectCreateRequest {
// mockProject creates a new project for testing and associates it with the supplied account ID.
func newMockProject(accountID string) *project.Project {
req := mockProjectCreateRequest(accountID)
p, err := project.Create(tests.Context(), auth.Claims{}, test.MasterDB, req, time.Now().UTC().AddDate(-1, -1, -1))
p, err := appCtx.ProjectRepo.Create(tests.Context(), auth.Claims{}, req, time.Now().UTC().AddDate(-1, -1, -1))
if err != nil {
panic(err)
}

View File

@ -50,13 +50,13 @@ func mockSignupRequest() signup.SignupRequest {
func newMockSignup() mockSignup {
req := mockSignupRequest()
now := time.Now().UTC().AddDate(-1, -1, -1)
s, err := signup.Signup(tests.Context(), auth.Claims{}, test.MasterDB, req, now)
s, err := appCtx.SignupRepo.Signup(tests.Context(), auth.Claims{}, req, now)
if err != nil {
panic(err)
}
expires := time.Now().UTC().Sub(s.User.CreatedAt) + time.Hour
tkn, err := user_auth.Authenticate(tests.Context(), test.MasterDB, authenticator, user_auth.AuthenticateRequest{
tkn, err := appCtx.AuthRepo.Authenticate(tests.Context(), user_auth.AuthenticateRequest{
Email: req.User.Email,
Password: req.User.Password,
}, expires, now)

View File

@ -5,6 +5,11 @@ import (
"context"
"encoding/json"
"fmt"
"geeks-accelerator/oss/saas-starter-kit/internal/account/account_preference"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/notify"
"geeks-accelerator/oss/saas-starter-kit/internal/project"
"geeks-accelerator/oss/saas-starter-kit/internal/project_route"
"geeks-accelerator/oss/saas-starter-kit/internal/user_account/invite"
"io"
"io/ioutil"
"net/http"
@ -31,9 +36,12 @@ import (
"github.com/pkg/errors"
)
var a http.Handler
var test *tests.Test
var authenticator *auth.Authenticator
var (
a http.Handler
test *tests.Test
authenticator *auth.Authenticator
appCtx *handlers.AppContext
)
// Information about the users we have created for testing.
type roleTest struct {
@ -84,18 +92,51 @@ func testMain(m *testing.M) int {
log := test.Log
log.SetOutput(ioutil.Discard)
a = handlers.API(shutdown, log, webcontext.Env_Dev, test.MasterDB, nil, authenticator)
projectRoute, err := project_route.New("http://web-api.com", "http://web-app.com")
if err != nil {
panic(err)
}
notifyEmail := notify.NewEmailDisabled()
usrRepo := user.MockRepository(test.MasterDB)
usrAccRepo := user_account.NewRepository(test.MasterDB)
accRepo := account.NewRepository(test.MasterDB)
accPrefRepo := account_preference.NewRepository(test.MasterDB)
authRepo := user_auth.NewRepository(test.MasterDB, authenticator, usrRepo, usrAccRepo, accPrefRepo)
signupRepo := signup.NewRepository(test.MasterDB, usrRepo, usrAccRepo, accRepo)
inviteRepo := invite.NewRepository(test.MasterDB, usrRepo, usrAccRepo, accRepo, projectRoute.UserInviteAccept, notifyEmail, "6368616e676520746869732070613434")
prjRepo := project.NewRepository(test.MasterDB)
appCtx = &handlers.AppContext{
Log: log,
Env: webcontext.Env_Dev,
MasterDB: test.MasterDB,
Redis: nil,
UserRepo: usrRepo,
UserAccountRepo: usrAccRepo,
AccountRepo: accRepo,
AccountPrefRepo: accPrefRepo,
AuthRepo: authRepo,
SignupRepo: signupRepo,
InviteRepo: inviteRepo,
ProjectRepo: prjRepo,
Authenticator: authenticator,
}
a = handlers.API(shutdown, appCtx)
// Create a new account directly business logic. This creates an
// initial account and user that we will use for admin validated endpoints.
signupReq1 := mockSignupRequest()
signup1, err := signup.Signup(tests.Context(), auth.Claims{}, test.MasterDB, signupReq1, now)
signup1, err := signupRepo.Signup(tests.Context(), auth.Claims{}, signupReq1, now)
if err != nil {
panic(err)
}
expires := time.Now().UTC().Sub(signup1.User.CreatedAt) + time.Hour
adminTkn, err := user_auth.Authenticate(tests.Context(), test.MasterDB, authenticator, user_auth.AuthenticateRequest{
adminTkn, err := authRepo.Authenticate(tests.Context(), user_auth.AuthenticateRequest{
Email: signupReq1.User.Email,
Password: signupReq1.User.Password,
}, expires, now)
@ -110,7 +151,7 @@ func testMain(m *testing.M) int {
// Create a second account that the first account user should not have access to.
signupReq2 := mockSignupRequest()
signup2, err := signup.Signup(tests.Context(), auth.Claims{}, test.MasterDB, signupReq2, now)
signup2, err := signupRepo.Signup(tests.Context(), auth.Claims{}, signupReq2, now)
if err != nil {
panic(err)
}
@ -134,12 +175,12 @@ func testMain(m *testing.M) int {
Password: "akTechFr0n!ier",
PasswordConfirm: "akTechFr0n!ier",
}
usr, err := user.Create(tests.Context(), adminClaims, test.MasterDB, userReq, now)
usr, err := usrRepo.Create(tests.Context(), adminClaims, userReq, now)
if err != nil {
panic(err)
}
_, err = user_account.Create(tests.Context(), adminClaims, test.MasterDB, user_account.UserAccountCreateRequest{
_, err = usrAccRepo.Create(tests.Context(), adminClaims, user_account.UserAccountCreateRequest{
UserID: usr.ID,
AccountID: signup1.Account.ID,
Roles: []user_account.UserAccountRole{user_account.UserAccountRole_User},
@ -149,7 +190,7 @@ func testMain(m *testing.M) int {
panic(err)
}
userTkn, err := user_auth.Authenticate(tests.Context(), test.MasterDB, authenticator, user_auth.AuthenticateRequest{
userTkn, err := authRepo.Authenticate(tests.Context(), user_auth.AuthenticateRequest{
Email: usr.Email,
Password: userReq.Password,
}, expires, now)

View File

@ -14,7 +14,6 @@ import (
"geeks-accelerator/oss/saas-starter-kit/internal/platform/tests"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/weberror"
"geeks-accelerator/oss/saas-starter-kit/internal/user"
"geeks-accelerator/oss/saas-starter-kit/internal/user_account"
"github.com/pborman/uuid"
)
@ -22,12 +21,12 @@ import (
// newMockUserAccount creates a new user user for testing and associates it with the supplied account ID.
func newMockUserAccount(accountID string, role user_account.UserAccountRole) *user_account.UserAccount {
req := mockUserCreateRequest()
u, err := user.Create(tests.Context(), auth.Claims{}, test.MasterDB, req, time.Now().UTC().AddDate(-1, -1, -1))
u, err := appCtx.UserRepo.Create(tests.Context(), auth.Claims{}, req, time.Now().UTC().AddDate(-1, -1, -1))
if err != nil {
panic(err)
}
ua, err := user_account.Create(tests.Context(), auth.Claims{}, test.MasterDB, user_account.UserAccountCreateRequest{
ua, err := appCtx.UserAccountRepo.Create(tests.Context(), auth.Claims{}, user_account.UserAccountCreateRequest{
UserID: u.ID,
AccountID: accountID,
Roles: []user_account.UserAccountRole{role},
@ -65,7 +64,7 @@ func TestUserAccountCRUDAdmin(t *testing.T) {
}
t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url)
newUser, err := user.Create(tests.Context(), auth.Claims{}, test.MasterDB, mockUserCreateRequest(), time.Now().UTC().AddDate(-1, -1, -1))
newUser, err := appCtx.UserRepo.Create(tests.Context(), auth.Claims{}, mockUserCreateRequest(), time.Now().UTC().AddDate(-1, -1, -1))
if err != nil {
t.Fatalf("\t%s\tCreate new user failed.", tests.Failed)
}

View File

@ -38,12 +38,12 @@ func mockUserCreateRequest() user.UserCreateRequest {
// mockUser creates a new user for testing and associates it with the supplied account ID.
func newMockUser(accountID string, role user_account.UserAccountRole) mockUser {
req := mockUserCreateRequest()
u, err := user.Create(tests.Context(), auth.Claims{}, test.MasterDB, req, time.Now().UTC().AddDate(-1, -1, -1))
u, err := appCtx.UserRepo.Create(tests.Context(), auth.Claims{}, req, time.Now().UTC().AddDate(-1, -1, -1))
if err != nil {
panic(err)
}
_, err = user_account.Create(tests.Context(), auth.Claims{}, test.MasterDB, user_account.UserAccountCreateRequest{
_, err = appCtx.UserAccountRepo.Create(tests.Context(), auth.Claims{}, user_account.UserAccountCreateRequest{
UserID: u.ID,
AccountID: accountID,
Roles: []user_account.UserAccountRole{role},
@ -126,7 +126,7 @@ func TestUserCRUDAdmin(t *testing.T) {
t.Logf("\t%s\tReceived expected result.", tests.Success)
// Only for user creation do we need to do this.
_, err := user_account.Create(tests.Context(), auth.Claims{}, test.MasterDB, user_account.UserAccountCreateRequest{
_, err := appCtx.UserAccountRepo.Create(tests.Context(), auth.Claims{}, user_account.UserAccountCreateRequest{
UserID: actual.ID,
AccountID: tr.Account.ID,
Roles: []user_account.UserAccountRole{user_account.UserAccountRole_User},
@ -401,7 +401,7 @@ func TestUserCRUDAdmin(t *testing.T) {
}
t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url)
_, err := user_account.Create(tests.Context(), auth.Claims{}, test.MasterDB, user_account.UserAccountCreateRequest{
_, err := appCtx.UserAccountRepo.Create(tests.Context(), auth.Claims{}, user_account.UserAccountCreateRequest{
UserID: tr.User.ID,
AccountID: newAccount.ID,
Roles: []user_account.UserAccountRole{user_account.UserAccountRole_User},
@ -805,7 +805,7 @@ func TestUserCRUDUser(t *testing.T) {
}
t.Logf("\tTest: %s - %s %s", rt.name, rt.method, rt.url)
_, err := user_account.Create(tests.Context(), auth.Claims{}, test.MasterDB, user_account.UserAccountCreateRequest{
_, err := appCtx.UserAccountRepo.Create(tests.Context(), auth.Claims{}, user_account.UserAccountCreateRequest{
UserID: tr.User.ID,
AccountID: newAccount.ID,
Roles: []user_account.UserAccountRole{user_account.UserAccountRole_User},